openai_client = $openai_client; } public function supported_post_types() { $post_types = array('page', 'post'); if (post_type_exists('product')) { $post_types[] = 'product'; } $post_types = apply_filters('aiseo_supported_post_types', $post_types); $post_types = array_values(array_unique(array_filter(array_map('sanitize_key', (array) $post_types)))); return array_values(array_filter($post_types, 'post_type_exists')); } public function excluded_scan_slugs() { static $cached_slugs = null; if (null !== $cached_slugs) { return $cached_slugs; } $slugs = array('checkout', 'cart', 'wishlist', 'compare'); $slugs = apply_filters('aiseo_excluded_scan_slugs', $slugs); $slugs = array_values(array_unique(array_filter(array_map('sanitize_title', (array) $slugs)))); $cached_slugs = $slugs; return $cached_slugs; } public function excluded_scan_post_ids() { static $cached_ids = null; if (null !== $cached_ids) { return $cached_ids; } $slugs = $this->excluded_scan_slugs(); $ids = array(); if ($slugs) { $ids = get_posts( array( 'post_type' => $this->supported_post_types(), 'post_status' => 'any', 'fields' => 'ids', 'posts_per_page' => -1, 'post_name__in' => $slugs, 'no_found_rows' => true, ) ); } if (function_exists('wc_get_page_id')) { foreach (array('cart', 'checkout') as $woocommerce_page) { $page_id = (int) wc_get_page_id($woocommerce_page); if ($page_id > 0) { $ids[] = $page_id; } } } $ids = apply_filters('aiseo_excluded_scan_post_ids', $ids, $slugs); $cached_ids = array_values(array_unique(array_filter(array_map('absint', (array) $ids)))); return $cached_ids; } public function is_scannable_post($post) { if (!$post instanceof WP_Post) { $post = get_post($post); } if (!$post || 'publish' !== $post->post_status || !in_array($post->post_type, $this->supported_post_types(), true)) { return false; } if (in_array(sanitize_title($post->post_name), $this->excluded_scan_slugs(), true)) { return false; } if (in_array((int) $post->ID, $this->excluded_scan_post_ids(), true)) { return false; } return (bool) apply_filters('aiseo_is_scannable_post', true, $post); } public function scan_post($post_id, $should_cancel = null) { $post_id = absint($post_id); $post = get_post($post_id); if (!$this->is_scannable_post($post)) { return new WP_Error('aiseo_invalid_post', __('This item cannot be scanned.', 'aivisibility-seo-geo-agent')); } $url = get_permalink($post); if (!$url) { return new WP_Error('aiseo_missing_permalink', __('Could not resolve the permalink for this item.', 'aivisibility-seo-geo-agent')); } $page_data = $this->extract_database_page_data($post, $url); $live_report = $this->extract_server_html_data($post, $url); if (is_wp_error($live_report)) { return $live_report; } $page_data = $this->merge_database_and_live_data($page_data, $live_report); $local_report = $this->build_local_report($page_data); $ai_payload = $this->build_ai_payload($post, $url, $page_data); $ai_report = $this->openai_client->analyze($ai_payload); if (is_callable($should_cancel) && call_user_func($should_cancel)) { return new WP_Error('aiseo_scan_stopped', __('Scan was stopped before the AI report was saved.', 'aivisibility-seo-geo-agent')); } $ai_error = ''; if (is_wp_error($ai_report)) { $ai_error = $ai_report->get_error_message(); $ai_report = null; } $score = is_array($ai_report) && isset($ai_report['score']) ? (int) $ai_report['score'] : (int) $local_report['score']; $scan_time = current_time('mysql'); $report = array( 'post_id' => $post_id, 'post_type' => $post->post_type, 'title' => get_the_title($post), 'edit_url' => get_edit_post_link($post_id, ''), 'url' => $url, 'score' => $score, 'scanned_at' => $scan_time, 'scan_source' => isset($page_data['source']) ? $page_data['source'] : 'database', 'source_label' => isset($page_data['source_label']) ? $page_data['source_label'] : __('WordPress database', 'aivisibility-seo-geo-agent'), 'live_html_status' => isset($page_data['live_html_status']) ? $page_data['live_html_status'] : 'disabled', 'live_html_error' => isset($page_data['live_html_error']) ? $page_data['live_html_error'] : '', 'local' => $page_data, 'local_report' => $local_report, 'ai_report' => $ai_report, 'ai_error' => $ai_error, 'model' => $this->openai_client->model(), ); if (is_callable($should_cancel) && call_user_func($should_cancel)) { return new WP_Error('aiseo_scan_stopped', __('Scan was stopped before the AI report was saved.', 'aivisibility-seo-geo-agent')); } update_post_meta($post_id, '_aiseo_report', $report); update_post_meta($post_id, '_aiseo_score', $score); update_post_meta($post_id, '_aiseo_scanned_at', $scan_time); return $report; } public function get_report($post_id) { $report = get_post_meta(absint($post_id), '_aiseo_report', true); return is_array($report) ? $report : array(); } public function test_curl_source($post_id) { $post_id = absint($post_id); $post = get_post($post_id); if (!$this->is_scannable_post($post)) { return new WP_Error('aiseo_invalid_post', __('This item cannot be scanned.', 'aivisibility-seo-geo-agent')); } $url = get_permalink($post); if (!$url) { return new WP_Error('aiseo_missing_permalink', __('Could not resolve the permalink for this item.', 'aivisibility-seo-geo-agent')); } $source = $this->extract_server_html_data($post, $url); if (is_wp_error($source)) { return $source; } return array( 'post_id' => $post_id, 'url' => $url, 'status' => 'ok', 'source_label' => isset($source['source_label']) ? $source['source_label'] : __('Server-side cURL page source', 'aivisibility-seo-geo-agent'), 'live_html_status' => isset($source['live_html_status']) ? $source['live_html_status'] : 'server_html', 'title' => isset($source['title']) ? $source['title'] : '', 'word_count' => isset($source['word_count']) ? (int) $source['word_count'] : 0, 'meta_tags' => !empty($source['meta_tags']) && is_array($source['meta_tags']) ? count($source['meta_tags']) : 0, 'json_ld_count' => isset($source['json_ld_count']) ? (int) $source['json_ld_count'] : 0, 'images_total' => !empty($source['images']['total']) ? (int) $source['images']['total'] : 0, ); } public function build_strategy_autofill_payload($requested_mode = '') { $requested_mode = sanitize_key((string) $requested_mode); if (!in_array($requested_mode, array('local', 'generic', 'mixed'), true)) { $requested_mode = sanitize_key((string) get_option('aiseo_geo_mode', 'mixed')); } if (!in_array($requested_mode, array('local', 'generic', 'mixed'), true)) { $requested_mode = 'mixed'; } $url = home_url('/'); $post = $this->homepage_post(); $page_data = array(); $page = array( 'id' => 0, 'type' => 'homepage', 'wordpress_title' => get_bloginfo('name'), 'slug' => '', 'url' => $url, ); if ($post instanceof WP_Post) { $page = array( 'id' => (int) $post->ID, 'type' => $post->post_type, 'wordpress_title' => get_the_title($post), 'slug' => $post->post_name, 'url' => $url, ); $database_data = $this->extract_database_page_data($post, $url); $live_data = $this->extract_server_html_data($post, $url); if (is_wp_error($live_data)) { return $live_data; } $page_data = $this->merge_database_and_live_data($database_data, $live_data); } else { $page_data = $this->extract_homepage_server_html_data($url); if (is_wp_error($page_data)) { return $page_data; } } return array( 'task' => 'strategy_autofill', 'scan_architecture' => 'homepage_curl_source_for_settings_autofill', 'primary_source' => isset($page_data['source']) ? $page_data['source'] : 'server_html', 'live_html_status' => isset($page_data['live_html_status']) ? $page_data['live_html_status'] : 'server_html', 'page' => $page, 'site' => array( 'name' => get_bloginfo('name'), 'description' => get_bloginfo('description'), 'home_url' => home_url('/'), 'language' => get_locale(), ), 'seo_signals' => $page_data, 'current_settings' => array( 'business_address' => (string) get_option('aiseo_business_address', ''), 'business_phone' => (string) get_option('aiseo_business_phone', ''), 'business_email' => (string) get_option('aiseo_business_email', ''), 'mode' => $requested_mode, 'selected_mode' => $requested_mode, 'seo_strategy' => $this->seo_strategy_payload($requested_mode), ), 'instructions' => array( 'phase' => 'settings_autofill_from_homepage', 'purpose' => 'Infer editable SEO Strategy settings from the homepage. Do not generate page-level recommendations.', 'selected_mode_lock' => 'Return seo_geo_mode exactly as current_settings.mode and prioritize filling the strategy fields for that selected mode.', 'output_language' => 'Use the dominant language of the supplied HTML/page text when writing field values.', 'language_policy' => array( 'strict' => true, 'detect_from' => 'Detect the dominant language from seo_signals.lang, seo_signals.title, seo_signals.meta_description, seo_signals.headings, seo_signals.visible_text_sample, and the supplied HTML/text evidence.', 'same_language_output' => 'Write all returned strategy field values in the detected dominant page/site language. Do not translate into another language because of keywords, business settings, API locale, or examples.', 'seo_strategy_exception' => 'Only use a different output language when the user explicitly declares a target language in the SEO Strategy/current settings, for example "English pages" or "Greek and English".', 'mixed_language_pages' => 'If the page is mixed-language, use the language that dominates the visible public content. Preserve brand names, product names, URLs, schema identifiers, and proper nouns as-is.', ), ), ); } public function get_scan_ids($single_post_id = 0) { if ($single_post_id) { $post = get_post(absint($single_post_id)); if ($this->is_scannable_post($post)) { return array(absint($single_post_id)); } return array(); } $ids = get_posts( array( 'post_type' => $this->supported_post_types(), 'post_status' => 'publish', 'fields' => 'ids', 'numberposts' => -1, 'post__not_in' => $this->excluded_scan_post_ids(), 'orderby' => array( 'type' => 'ASC', 'title' => 'ASC', ), ) ); return array_values(array_filter(array_map('absint', $ids), array($this, 'is_scannable_post'))); } private function live_html_enabled(WP_Post $post) { return (bool) apply_filters('aiseo_enable_live_html_scan', true, $post); } private function extract_server_html_data(WP_Post $post, $url) { if (!$this->live_html_enabled($post)) { return new WP_Error( 'aiseo_curl_scan_disabled', sprintf(__('%s cURL source scan is disabled. Enable server-side cURL/live HTML scan before running Scan & Report.', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME) ); } return $this->extract_server_html_data_from_url($url, $this->base_page_data($url, $post, 'server_html')); } private function extract_homepage_server_html_data($url) { return $this->extract_server_html_data_from_url( $url, $this->base_page_data_from_values($url, get_bloginfo('name'), 'homepage', 'server_html') ); } private function extract_server_html_data_from_url($url, array $data) { $html = $this->fetch_html($url); if (is_wp_error($html)) { return $html; } $data['source_label'] = __('Server-side cURL page source', 'aivisibility-seo-geo-agent'); $data['live_html_status'] = 'server_html'; $data['live_html_error'] = ''; $data['source_note'] = __('Full public HTML source was fetched server-side with WordPress HTTP API/cURL.', 'aivisibility-seo-geo-agent'); $data = $this->extract_html_document_signals($this->limit_raw_html($html, 300000), $data, true); if (empty($data['head_html_sample']) && empty($data['visible_text_sample'])) { return new WP_Error( 'aiseo_curl_parse_failed', $this->curl_failure_message( $url, sprintf(__('The public page source was fetched, but %s could not parse useful HTML from it.', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME) ) ); } return $data; } private function fetch_html($url) { $response = wp_remote_get( $url, array( 'timeout' => (int) apply_filters('aiseo_live_html_timeout', 10), 'redirection' => 3, 'limit_response_size' => (int) apply_filters('aiseo_live_html_limit_response_size', 300000), 'headers' => array( 'Accept' => 'text/html,application/xhtml+xml', 'User-Agent' => AISEO_AUDITOR_NAME . '/' . AISEO_AUDITOR_VERSION . '; ' . home_url('/'), ), ) ); if (is_wp_error($response)) { return new WP_Error( 'aiseo_curl_blocked', $this->curl_failure_message($url, $response->get_error_message()) ); } $status_code = (int) wp_remote_retrieve_response_code($response); if ($status_code < 200 || $status_code >= 300) { $message = wp_remote_retrieve_response_message($response); $body = $this->limit_text(wp_strip_all_tags((string) wp_remote_retrieve_body($response)), 220); $details = sprintf( __('HTTP %1$d %2$s. Response sample: %3$s', 'aivisibility-seo-geo-agent'), $status_code, $message ? $message : __('Unknown status', 'aivisibility-seo-geo-agent'), $body ? $body : __('empty response body', 'aivisibility-seo-geo-agent') ); return new WP_Error( 'aiseo_curl_http_blocked', $this->curl_failure_message($url, $details) ); } $html = (string) wp_remote_retrieve_body($response); if ('' === trim($html)) { return new WP_Error( 'aiseo_curl_empty_html', $this->curl_failure_message($url, __('The server returned an empty HTML response.', 'aivisibility-seo-geo-agent')) ); } return $html; } private function curl_failure_message($url, $details) { return sprintf( __('%4$s could not read the public HTML source with server-side cURL/WordPress HTTP API. Details: %1$s. Please allow server-side cURL requests from this website/server to the public page URL. If a firewall, WAF, security plugin, CDN, or Cloudflare is active, allowlist the %4$s request/User-Agent "%4$s/%2$s" for this URL: %3$s', 'aivisibility-seo-geo-agent'), $details, AISEO_AUDITOR_VERSION, esc_url_raw($url), AISEO_AUDITOR_NAME ); } private function extract_database_page_data(WP_Post $post, $url) { $rendered_content = $this->render_post_content($post); $manual_excerpt = $this->normalize_text($post->post_excerpt); $generated_excerpt = $this->normalize_text(wp_trim_words(wp_strip_all_tags(strip_shortcodes($post->post_content)), 28, '')); $description_candidate = '' !== $manual_excerpt ? $manual_excerpt : $generated_excerpt; $data = $this->base_page_data($url, $post, 'database'); $data['source_label'] = __('WordPress database', 'aivisibility-seo-geo-agent'); $data['source_note'] = __('Database content snapshot. Public page source must be fetched with server-side cURL before AI analysis can run.', 'aivisibility-seo-geo-agent'); $data['live_html_status'] = 'disabled'; $data['title'] = get_the_title($post); $data['title_length'] = $this->string_length($data['title']); $data['title_source'] = 'wordpress_post_title'; $data['meta_description'] = $description_candidate; $data['meta_description_length'] = $this->string_length($description_candidate); $data['meta_description_source'] = '' !== $manual_excerpt ? 'wordpress_manual_excerpt' : 'generated_excerpt_candidate'; $data['has_manual_excerpt'] = '' !== $manual_excerpt; $data['canonical_candidate'] = $url; $data['post'] = array( 'id' => (int) $post->ID, 'slug' => $post->post_name, 'status' => $post->post_status, 'type' => $post->post_type, 'date_gmt' => $post->post_date_gmt, 'modified_gmt' => $post->post_modified_gmt, 'raw_content_length' => $this->string_length($post->post_content), 'raw_content_sample' => $this->limit_text(wp_strip_all_tags(strip_shortcodes($post->post_content)), 3500), 'rendered_content_sample' => $this->limit_text(wp_strip_all_tags($rendered_content), 5500), ); $data['unavailable_in_db_scan'] = array( 'final_title_tag', 'final_meta_description_tag', 'canonical_tag', 'robots_meta_tag', 'open_graph_tags', 'twitter_card_tags', 'json_ld_schema_from_theme_or_seo_plugins', 'theme_header_footer_html', ); $data = $this->extract_content_signals($rendered_content, $data); $data = $this->add_featured_image_signal($post, $data); return $data; } private function base_page_data($url, WP_Post $post, $source) { return $this->base_page_data_from_values($url, get_the_title($post), $post->post_type, $source); } private function base_page_data_from_values($url, $title, $post_type, $source) { return array( 'source' => $source, 'source_label' => $source, 'source_note' => '', 'url' => $url, 'post_title' => (string) $title, 'post_type' => (string) $post_type, 'title' => '', 'title_length' => 0, 'title_source' => '', 'meta_description' => '', 'meta_description_length' => 0, 'meta_description_source' => '', 'has_manual_excerpt' => false, 'canonical' => '', 'canonical_candidate' => '', 'robots' => '', 'viewport' => '', 'lang' => '', 'meta_tags' => array(), 'link_tags' => array(), 'open_graph' => array(), 'twitter' => array(), 'json_ld_count' => 0, 'json_ld_types' => array(), 'json_ld' => array(), 'headings' => array( 'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(), ), 'images' => array( 'total' => 0, 'missing_alt' => 0, 'empty_alt' => 0, 'items' => array(), 'samples_missing_alt' => array(), 'featured_image' => null, ), 'links' => array( 'internal' => 0, 'external' => 0, ), 'word_count' => 0, 'visible_text_sample' => '', 'head_html_sample' => '', 'live_html_status' => 'disabled', 'live_html_error' => '', 'unavailable_in_db_scan' => array(), ); } private function homepage_post() { if ('page' === (string) get_option('show_on_front')) { $front_page_id = absint(get_option('page_on_front')); if ($front_page_id) { $post = get_post($front_page_id); if ($post instanceof WP_Post && 'publish' === $post->post_status && in_array($post->post_type, $this->supported_post_types(), true)) { return $post; } } } $post_id = absint(url_to_postid(home_url('/'))); if ($post_id) { $post = get_post($post_id); if ($post instanceof WP_Post && 'publish' === $post->post_status && in_array($post->post_type, $this->supported_post_types(), true)) { return $post; } } return null; } private function render_post_content(WP_Post $post) { $previous_post = isset($GLOBALS['post']) ? $GLOBALS['post'] : null; $GLOBALS['post'] = $post; setup_postdata($post); $content = apply_filters('the_content', $post->post_content); wp_reset_postdata(); if ($previous_post instanceof WP_Post) { $GLOBALS['post'] = $previous_post; } return (string) $content; } private function extract_html_document_signals($html, array $data, $include_head) { if (!class_exists('DOMDocument')) { $data['visible_text_sample'] = $this->limit_text(wp_strip_all_tags($html), 6000); $data['word_count'] = $this->word_count($data['visible_text_sample']); return $data; } $dom = $this->load_dom($html); if (!$dom) { $data['visible_text_sample'] = $this->limit_text(wp_strip_all_tags($html), 6000); $data['word_count'] = $this->word_count($data['visible_text_sample']); return $data; } if ($include_head) { $data = $this->extract_head_signals($dom, $data); } return $this->extract_content_signals_from_dom($dom, $data); } private function extract_content_signals($html, array $data) { if (!class_exists('DOMDocument')) { $data['visible_text_sample'] = $this->limit_text(wp_strip_all_tags($html), 6000); $data['word_count'] = $this->word_count($data['visible_text_sample']); return $data; } $dom = $this->load_dom('
' . $html . '
'); if (!$dom) { $data['visible_text_sample'] = $this->limit_text(wp_strip_all_tags($html), 6000); $data['word_count'] = $this->word_count($data['visible_text_sample']); return $data; } return $this->extract_content_signals_from_dom($dom, $data); } private function load_dom($html) { $previous = libxml_use_internal_errors(true); $dom = new DOMDocument(); $loaded = $dom->loadHTML('' . (string) $html); libxml_clear_errors(); libxml_use_internal_errors($previous); return $loaded ? $dom : null; } private function extract_head_signals(DOMDocument $dom, array $data) { $title_nodes = $dom->getElementsByTagName('title'); if ($title_nodes->length > 0) { $data['title'] = $this->normalize_text($title_nodes->item(0)->textContent); $data['title_length'] = $this->string_length($data['title']); $data['title_source'] = 'live_title_tag'; } $html_node = $dom->getElementsByTagName('html')->item(0); if ($html_node instanceof DOMElement) { $data['lang'] = $html_node->getAttribute('lang'); } foreach ($dom->getElementsByTagName('meta') as $meta) { if (!$meta instanceof DOMElement) { continue; } $name = strtolower($meta->getAttribute('name')); $property = strtolower($meta->getAttribute('property')); $http_equiv = strtolower($meta->getAttribute('http-equiv')); $content = $this->normalize_text($meta->getAttribute('content')); if ($content && count($data['meta_tags']) < 100) { if ('' !== $property) { $data['meta_tags'][] = array( 'attribute' => 'property', 'name' => $property, 'content' => $this->limit_text($content, 600), ); } elseif ('' !== $http_equiv) { $data['meta_tags'][] = array( 'attribute' => 'http-equiv', 'name' => $http_equiv, 'content' => $this->limit_text($content, 600), ); } elseif ('' !== $name) { $data['meta_tags'][] = array( 'attribute' => 'name', 'name' => $name, 'content' => $this->limit_text($content, 600), ); } } if ('description' === $name) { $data['meta_description'] = $content; $data['meta_description_length'] = $this->string_length($content); $data['meta_description_source'] = 'live_meta_description_tag'; } elseif ('robots' === $name) { $data['robots'] = $content; } elseif ('viewport' === $name) { $data['viewport'] = $content; } if (0 === strpos($property, 'og:')) { $data['open_graph'][$property] = $content; } if (0 === strpos($name, 'twitter:')) { $data['twitter'][$name] = $content; } } foreach ($dom->getElementsByTagName('link') as $link) { if (!$link instanceof DOMElement) { continue; } if ('canonical' === strtolower($link->getAttribute('rel'))) { $data['canonical'] = esc_url_raw($link->getAttribute('href')); } if (count($data['link_tags']) < 60) { $rel = $this->normalize_text($link->getAttribute('rel')); $href = esc_url_raw($link->getAttribute('href')); if ('' !== $rel || '' !== $href) { $data['link_tags'][] = array( 'rel' => $this->limit_text($rel, 120), 'href' => $href, 'hreflang' => $this->limit_text($this->normalize_text($link->getAttribute('hreflang')), 40), 'type' => $this->limit_text($this->normalize_text($link->getAttribute('type')), 100), 'title' => $this->limit_text($this->normalize_text($link->getAttribute('title')), 160), ); } } } foreach ($dom->getElementsByTagName('script') as $script) { if (!$script instanceof DOMElement || 'application/ld+json' !== strtolower(trim($script->getAttribute('type')))) { continue; } $data['json_ld_count']++; $decoded_json = json_decode(trim($script->textContent), true); $types = $this->extract_json_ld_types($decoded_json); $data['json_ld_types'] = array_values(array_unique(array_merge($data['json_ld_types'], $types))); if (count($data['json_ld']) < 10) { $data['json_ld'][] = array( 'types' => $types, 'decoded' => is_array($decoded_json) ? $decoded_json : null, 'raw_sample' => $this->limit_text(trim($script->textContent), 12000), ); } } $head = $dom->getElementsByTagName('head')->item(0); if ($head) { $data['head_html_sample'] = $this->limit_text($dom->saveHTML($head), 5000); } return $data; } private function extract_content_signals_from_dom(DOMDocument $dom, array $data) { foreach (array('h1', 'h2', 'h3', 'h4', 'h5', 'h6') as $tag) { foreach ($dom->getElementsByTagName($tag) as $heading) { $text = $this->normalize_text($heading->textContent); if ('' !== $text) { $data['headings'][$tag][] = $this->limit_text($text, 180); } } } $host = wp_parse_url(home_url('/'), PHP_URL_HOST); foreach ($dom->getElementsByTagName('a') as $anchor) { if (!$anchor instanceof DOMElement) { continue; } $href = $anchor->getAttribute('href'); if (!$href || '#' === $href[0] || 0 === strpos($href, 'mailto:') || 0 === strpos($href, 'tel:')) { continue; } $link_host = wp_parse_url($href, PHP_URL_HOST); if (!$link_host || $link_host === $host) { $data['links']['internal']++; } else { $data['links']['external']++; } } foreach ($dom->getElementsByTagName('img') as $img) { if (!$img instanceof DOMElement) { continue; } $data['images']['total']++; $has_alt = $img->hasAttribute('alt'); $alt = $has_alt ? trim($img->getAttribute('alt')) : ''; $this->add_image_item_sample($data, $img, $alt, $has_alt); if (!$has_alt) { $data['images']['missing_alt']++; $this->add_missing_image_sample($data, $img); } elseif ('' === $alt) { $data['images']['empty_alt']++; $this->add_missing_image_sample($data, $img); } } $this->remove_non_content_nodes($dom); $body = $dom->getElementsByTagName('body')->item(0); $visible_text = $body ? $this->normalize_text($body->textContent) : $this->normalize_text($dom->textContent); $data['visible_text_sample'] = $this->limit_text($visible_text, 6500); $data['word_count'] = $this->word_count($visible_text); return $data; } private function add_featured_image_signal(WP_Post $post, array $data) { $thumbnail_id = get_post_thumbnail_id($post); if (!$thumbnail_id) { return $data; } $alt = trim((string) get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true)); $src = wp_get_attachment_url($thumbnail_id); $data['images']['total']++; $data['images']['featured_image'] = array( 'id' => (int) $thumbnail_id, 'src' => $src ? esc_url_raw($src) : '', 'alt' => $alt, ); $data['images']['items'][] = array( 'id' => (int) $thumbnail_id, 'attachment_id' => (int) $thumbnail_id, 'src' => $src ? esc_url_raw($src) : '', 'alt' => $alt, 'has_alt' => '' !== $alt, 'class' => 'featured-image', 'role' => 'featured', ); if ('' === $alt) { $data['images']['empty_alt']++; if (count($data['images']['samples_missing_alt']) < 6) { $data['images']['samples_missing_alt'][] = array( 'id' => (int) $thumbnail_id, 'attachment_id' => (int) $thumbnail_id, 'src' => $src ? esc_url_raw($src) : '', 'class' => 'featured-image', ); } } return $data; } private function merge_database_and_live_data(array $database_data, array $live_data) { $live_data['source'] = 'hybrid'; $live_data['source_label'] = __('Database + cURL page source', 'aivisibility-seo-geo-agent'); $live_data['source_note'] = __('DB content was collected first, then the public page source was read server-side with cURL.', 'aivisibility-seo-geo-agent'); $live_data['database_content'] = array( 'title' => $database_data['title'], 'meta_description' => $database_data['meta_description'], 'word_count' => $database_data['word_count'], 'headings' => $database_data['headings'], 'images' => $database_data['images'], 'links' => $database_data['links'], 'visible_text_sample' => $database_data['visible_text_sample'], ); if (empty($live_data['meta_description']) && !empty($database_data['meta_description'])) { $live_data['meta_description'] = $database_data['meta_description']; $live_data['meta_description_length'] = $database_data['meta_description_length']; $live_data['meta_description_source'] = $database_data['meta_description_source']; } if (!empty($database_data['images']) && is_array($database_data['images'])) { if (empty($live_data['images']['featured_image']) && !empty($database_data['images']['featured_image'])) { $live_data['images']['featured_image'] = $database_data['images']['featured_image']; } $database_items = !empty($database_data['images']['items']) && is_array($database_data['images']['items']) ? $database_data['images']['items'] : array(); $live_items = !empty($live_data['images']['items']) && is_array($live_data['images']['items']) ? $live_data['images']['items'] : array(); $live_data['images']['items'] = $this->merge_image_items($live_items, $database_items); } return $live_data; } private function build_ai_payload(WP_Post $post, $url, array $page_data) { $selected_mode = sanitize_key((string) get_option('aiseo_geo_mode', 'mixed')); if (!in_array($selected_mode, array('local', 'generic', 'mixed'), true)) { $selected_mode = 'mixed'; } $seo_strategy = $this->seo_strategy_payload($selected_mode); return array( 'scan_architecture' => 'db_snapshot_plus_required_server_side_curl_page_source', 'primary_source' => isset($page_data['source']) ? $page_data['source'] : 'database', 'live_html_status' => isset($page_data['live_html_status']) ? $page_data['live_html_status'] : 'disabled', 'page' => array( 'id' => (int) $post->ID, 'type' => $post->post_type, 'wordpress_title' => get_the_title($post), 'slug' => $post->post_name, 'url' => $url, 'commerce' => $this->commerce_payload($post), ), 'seo_signals' => $page_data, 'schema_context' => $this->schema_context_payload($post, $url, $page_data), 'seo_geo_settings' => array( 'business_address' => (string) get_option('aiseo_business_address', ''), 'business_phone' => (string) get_option('aiseo_business_phone', ''), 'business_email' => (string) get_option('aiseo_business_email', ''), 'mode' => $selected_mode, 'selected_mode' => $selected_mode, ), 'seo_strategy' => $seo_strategy, 'instructions' => array( 'phase' => 'seo_geo_scan_and_report', 'primary_recommendations' => 'All improvement recommendations should come from the AI analysis.', 'business_context_requirement' => 'Before writing metadata, headings, JSON-LD, or image alt values, evaluate seo_geo_settings.business_address, seo_geo_settings.business_phone, seo_geo_settings.business_email, and seo_strategy.active_strategy as the authoritative business context unless contradicted by verified page facts.', 'selected_strategy_requirement' => 'Use seo_strategy.active_strategy as the selected SEO-GEO strategy for this page scan. Do not optimize against a different mode-specific strategy group.', 'recommendation_scope' => 'Report recommendations for metatags, Open Graph/Twitter social tags, JSON-LD, page headings, and image alt text. Paragraph/body copy is outside this report scope.', 'heading_recommendation_scope' => 'Return suggested_heading_updates for headings only. Include useful visible h1-h6 headings from seo_signals.headings, and from seo_signals.editable_headings when present. Do not stop at H1/H2; include H3, H4, H5, and H6 whenever they exist in the payload. Each update must include tag, zero-based index within that tag, exact current_text, suggested_text, seo_scope, and reason. Preserve nested markup in the source heading when describing the recommendation.', 'curl_source_requirement' => 'AI analysis runs only after the public page source is readable through server-side cURL/WordPress HTTP API.', 'json_ld_requirement' => 'For every scanned URL, return suggested_json_ld as complete valid Schema.org JSON-LD. Prefer a single object with @context and @graph. Include as many supported entities as the page evidence allows: WebSite, Organization/LocalBusiness/ProfessionalService, WebPage, BreadcrumbList, Article/BlogPosting, Service, Product, Offer, AggregateRating, Review, ImageObject, and VideoObject. Product, Offer, AggregateRating, and Review are allowed only when supported by page.commerce, existing JSON-LD, visible page content, or supplied business/SEO strategy facts. FAQPage is allowed only when the current page already contains explicit FAQ question-and-answer pairs or existing valid FAQPage JSON-LD. Do not invent prices, ratings, reviews, stock, SKUs, FAQ questions, FAQ answers, addresses, phones, or emails.', 'output_language' => 'Use the dominant language of the supplied HTML/page text for every generated text field.', 'language_policy' => array( 'strict' => true, 'detect_from' => 'Detect the dominant language from seo_signals.lang, seo_signals.title, seo_signals.meta_description, seo_signals.headings, seo_signals.visible_text_sample, and any supplied HTML/text evidence.', 'same_language_output' => 'All generated text must stay in the detected dominant language: title, meta description, Open Graph/Twitter text, headings, image alt text, JSON-LD names/descriptions, FAQ text, report reasons, and recommendations.', 'no_locale_override' => 'Do not switch language because of WordPress locale, API locale, example text, keyword lists, or business settings when the visible page HTML clearly uses another dominant language.', 'seo_strategy_exception' => 'Only use a different language when seo_strategy.active_strategy explicitly names the target language for this exact page, for example "English pages" or "Greek and English".', 'mixed_language_pages' => 'If the page is mixed-language, use the language that dominates the visible public content. Preserve brand names, product names, URLs, schema identifiers, and proper nouns as-is.', ), ), ); } private function schema_context_payload(WP_Post $post, $url, array $page_data) { return array( 'required_output' => 'suggested_json_ld must be a complete JSON-LD object for this exact page, not only a type label or recommendation.', 'preferred_shape' => 'Use {"@context":"https://schema.org","@graph":[...]} with stable @id values based on the page URL and home URL.', 'facts_policy' => 'Use only verified page facts, existing JSON-LD, WordPress post data, WooCommerce product data, seo_geo_settings, and seo_strategy. If a rich entity is not supported by evidence, omit that entity instead of guessing. FAQPage requires explicit current-page FAQ question-and-answer evidence; never create FAQ schema from strategy, headings, keywords, or assumptions.', 'page_url' => esc_url_raw($url), 'post_type' => $post->post_type, 'existing_json_ld_types' => !empty($page_data['json_ld_types']) && is_array($page_data['json_ld_types']) ? array_values($page_data['json_ld_types']) : array(), 'faq_candidates' => $this->faq_candidates_from_page_data($page_data), 'commerce' => $this->commerce_payload($post), ); } private function commerce_payload(WP_Post $post) { $payload = array( 'is_product_page' => 'product' === $post->post_type, 'woocommerce_available' => function_exists('wc_get_product'), ); if ('product' !== $post->post_type || !function_exists('wc_get_product')) { return $payload; } $product = wc_get_product($post->ID); if (!$product) { return $payload; } $images = array(); $main_image_id = method_exists($product, 'get_image_id') ? absint($product->get_image_id()) : 0; $image_ids = $main_image_id ? array($main_image_id) : array(); if (method_exists($product, 'get_gallery_image_ids')) { $image_ids = array_merge($image_ids, array_map('absint', (array) $product->get_gallery_image_ids())); } foreach (array_slice(array_values(array_unique(array_filter($image_ids))), 0, 8) as $image_id) { $image_url = wp_get_attachment_image_url($image_id, 'full'); if ($image_url) { $images[] = esc_url_raw($image_url); } } $attributes = array(); if (method_exists($product, 'get_attributes')) { foreach ((array) $product->get_attributes() as $attribute) { if (!is_object($attribute) || !method_exists($attribute, 'get_name')) { continue; } $name = (string) $attribute->get_name(); $label = function_exists('wc_attribute_label') ? wc_attribute_label($name) : $name; $value = method_exists($product, 'get_attribute') ? $this->limit_text((string) $product->get_attribute($name), 240) : ''; if ('' !== $value) { $attributes[] = array( 'name' => $this->limit_text($label, 120), 'value' => $value, ); } if (count($attributes) >= 12) { break; } } } $categories = taxonomy_exists('product_cat') ? wp_get_post_terms($post->ID, 'product_cat', array('fields' => 'names')) : array(); $tags = taxonomy_exists('product_tag') ? wp_get_post_terms($post->ID, 'product_tag', array('fields' => 'names')) : array(); return array_merge($payload, array( 'product_type' => method_exists($product, 'get_type') ? (string) $product->get_type() : '', 'name' => method_exists($product, 'get_name') ? $this->limit_text((string) $product->get_name(), 220) : get_the_title($post), 'sku' => method_exists($product, 'get_sku') ? $this->limit_text((string) $product->get_sku(), 120) : '', 'price' => method_exists($product, 'get_price') ? (string) $product->get_price() : '', 'regular_price' => method_exists($product, 'get_regular_price') ? (string) $product->get_regular_price() : '', 'sale_price' => method_exists($product, 'get_sale_price') ? (string) $product->get_sale_price() : '', 'currency' => function_exists('get_woocommerce_currency') ? (string) get_woocommerce_currency() : '', 'stock_status' => method_exists($product, 'get_stock_status') ? (string) $product->get_stock_status() : '', 'in_stock' => method_exists($product, 'is_in_stock') ? (bool) $product->is_in_stock() : null, 'stock_quantity' => method_exists($product, 'get_stock_quantity') ? $product->get_stock_quantity() : null, 'average_rating' => method_exists($product, 'get_average_rating') ? (string) $product->get_average_rating() : '', 'rating_count' => method_exists($product, 'get_rating_count') ? (int) $product->get_rating_count() : 0, 'review_count' => method_exists($product, 'get_review_count') ? (int) $product->get_review_count() : 0, 'short_description' => method_exists($product, 'get_short_description') ? $this->limit_text(wp_strip_all_tags($product->get_short_description()), 900) : '', 'description' => method_exists($product, 'get_description') ? $this->limit_text(wp_strip_all_tags($product->get_description()), 1200) : '', 'categories' => is_wp_error($categories) ? array() : array_slice(array_values(array_filter(array_map('strval', (array) $categories))), 0, 12), 'tags' => is_wp_error($tags) ? array() : array_slice(array_values(array_filter(array_map('strval', (array) $tags))), 0, 16), 'images' => $images, 'attributes' => $attributes, )); } private function faq_candidates_from_page_data(array $page_data) { $text = ''; foreach (array('visible_text_sample', 'rendered_content_sample', 'raw_content_sample') as $key) { if (!empty($page_data[$key]) && is_string($page_data[$key])) { $text .= ' ' . $page_data[$key]; } elseif (!empty($page_data['post'][$key]) && is_string($page_data['post'][$key])) { $text .= ' ' . $page_data['post'][$key]; } } $text = $this->normalize_text($text); if ('' === $text) { return array(); } $sentences = preg_split('/(?<=[\.;!\?])\s+/u', $text); $candidates = array(); $total = is_array($sentences) ? count($sentences) : 0; for ($i = 0; $i < $total; $i++) { $question = $this->normalize_text($sentences[$i]); if ('' === $question || !preg_match('/[\?;]$/u', $question)) { continue; } if ($this->string_length($question) < 12 || $this->string_length($question) > 180) { continue; } $answer = isset($sentences[$i + 1]) ? $this->normalize_text($sentences[$i + 1]) : ''; if ($this->string_length($answer) < 20 || $this->string_length($answer) > 420) { continue; } $candidates[] = array( 'question' => $this->limit_text($question, 180), 'answer_candidate' => $this->limit_text($answer, 420), ); if (count($candidates) >= 8) { break; } } return $candidates; } private function seo_strategy_payload($declared_mode = '') { $declared_mode = sanitize_key((string) $declared_mode); if (!in_array($declared_mode, array('local', 'generic', 'mixed'), true)) { $declared_mode = sanitize_key((string) get_option('aiseo_geo_mode', 'mixed')); } if (!in_array($declared_mode, array('local', 'generic', 'mixed'), true)) { $declared_mode = 'mixed'; } $strategy = get_option('aiseo_seo_strategy', array()); if (!is_array($strategy)) { $strategy = array(); } $payload = array( 'declared_mode' => $declared_mode, 'common' => array( 'business_description' => $this->strategy_value($strategy, 'business_description'), 'main_service_or_product' => $this->strategy_value($strategy, 'main_offer'), 'target_audience' => $this->strategy_value($strategy, 'target_audience'), 'conversion_goal' => $this->strategy_value($strategy, 'conversion_goal'), 'preferred_tone' => $this->strategy_value($strategy, 'tone'), 'keywords_to_avoid' => $this->strategy_value($strategy, 'avoid_keywords'), ), 'local' => array( 'main_city_or_area' => $this->strategy_value($strategy, 'local_main_area'), 'service_areas' => $this->strategy_value($strategy, 'local_service_areas'), 'local_services' => $this->strategy_value($strategy, 'local_services'), 'local_keyword_strategy' => $this->strategy_value($strategy, 'local_keyword_strategy', 'ai_decide'), 'schema_focus' => $this->strategy_value($strategy, 'local_schema_focus', 'ai_decide'), ), 'generic' => array( 'target_country_or_language' => $this->strategy_value($strategy, 'generic_target_market'), 'main_topical_category' => $this->strategy_value($strategy, 'generic_topic_category'), 'intent_focus' => $this->strategy_value($strategy, 'generic_intent_focus', 'ai_decide'), 'primary_generic_keywords' => $this->strategy_value($strategy, 'generic_primary_keywords'), 'entity_topic_terms' => $this->strategy_value($strategy, 'generic_entity_terms'), 'local_terms_handling' => $this->strategy_value($strategy, 'generic_local_terms', 'avoid'), ), 'mixed' => array( 'primary_generic_keyword' => $this->strategy_value($strategy, 'mixed_primary_generic_keyword'), 'local_modifier_strategy' => $this->strategy_value($strategy, 'mixed_local_modifier_strategy', 'ai_decide'), 'priority_split' => $this->strategy_value($strategy, 'mixed_priority_split', 'balanced'), 'pages_that_stay_generic' => $this->strategy_value($strategy, 'mixed_generic_pages'), 'pages_with_local_signals' => $this->strategy_value($strategy, 'mixed_local_pages'), 'schema_mix' => $this->strategy_value($strategy, 'mixed_schema_mix', 'ai_decide'), ), ); $payload['active_strategy'] = array( 'mode' => $declared_mode, 'common' => $payload['common'], 'mode_specific' => isset($payload[$declared_mode]) ? $payload[$declared_mode] : array(), ); return $payload; } private function strategy_value(array $strategy, $key, $default = '') { return isset($strategy[$key]) ? (string) $strategy[$key] : (string) $default; } private function build_local_report(array $data) { $checks = array(); $priority_actions = array(); $score = 100; $is_live_available = in_array($data['source'], array('live_html', 'hybrid'), true); $this->local_check( $checks, $priority_actions, $score, 'meta', $data['title_length'] > 0 && $data['title_length'] <= 65, __('Title candidate exists and has a reasonable length.', 'aivisibility-seo-geo-agent'), __('Use a clear SEO title around 50-60 characters with the main keyword near the beginning.', 'aivisibility-seo-geo-agent'), 'high', $data['title_length'] ? 6 : 12 ); $this->local_check( $checks, $priority_actions, $score, 'meta', !empty($data['has_manual_excerpt']) && $data['meta_description_length'] >= 80 && $data['meta_description_length'] <= 160, __('Manual excerpt can support a useful meta description candidate.', 'aivisibility-seo-geo-agent'), __('Add a manual excerpt or SEO meta description candidate around 120-155 characters.', 'aivisibility-seo-geo-agent'), 'medium', 8 ); $h2_h3_count = count($data['headings']['h2']) + count($data['headings']['h3']); $this->local_check( $checks, $priority_actions, $score, 'headings', $h2_h3_count > 0 || (int) $data['word_count'] < 250, __('Content headings provide structure.', 'aivisibility-seo-geo-agent'), __('Add descriptive H2/H3 headings to structure the page and include related keywords naturally.', 'aivisibility-seo-geo-agent'), 'medium', 8 ); $missing_alt_total = (int) $data['images']['missing_alt'] + (int) $data['images']['empty_alt']; $this->local_check( $checks, $priority_actions, $score, 'images', 0 === $missing_alt_total, __('Detected images have alt attributes.', 'aivisibility-seo-geo-agent'), __('Add descriptive alt text to meaningful content and featured images.', 'aivisibility-seo-geo-agent'), 'medium', min(12, 4 + $missing_alt_total) ); $this->local_check( $checks, $priority_actions, $score, 'content', (int) $data['word_count'] >= 250, __('The content has enough visible text for a basic SEO evaluation.', 'aivisibility-seo-geo-agent'), __('Expand thin content with helpful sections, examples, FAQs, and internal links that match search intent.', 'aivisibility-seo-geo-agent'), 'medium', 10 ); $this->local_check( $checks, $priority_actions, $score, 'content', (int) $data['links']['internal'] > 0 || (int) $data['word_count'] < 250, __('Content includes internal linking or is too short to require it.', 'aivisibility-seo-geo-agent'), __('Add relevant internal links to supporting pages and conversion pages.', 'aivisibility-seo-geo-agent'), 'low', 4 ); if ($is_live_available) { $this->local_check( $checks, $priority_actions, $score, 'open_graph', !empty($data['open_graph']['og:title']) && !empty($data['open_graph']['og:description']) && !empty($data['open_graph']['og:image']), __('Core Open Graph tags are present in live HTML.', 'aivisibility-seo-geo-agent'), __('Add og:title, og:description, and og:image so shares have controlled titles, copy, and preview images.', 'aivisibility-seo-geo-agent'), 'medium', 8 ); $this->local_check( $checks, $priority_actions, $score, 'json_ld', $data['json_ld_count'] > 0, __('JSON-LD structured data was found in live HTML.', 'aivisibility-seo-geo-agent'), __('Add relevant JSON-LD such as WebPage, Article, Organization, LocalBusiness, FAQPage, or BreadcrumbList where appropriate.', 'aivisibility-seo-geo-agent'), 'medium', 9 ); $this->local_check( $checks, $priority_actions, $score, 'technical', '' !== $data['canonical'], __('Canonical tag was found in live HTML.', 'aivisibility-seo-geo-agent'), __('Add a canonical URL to reduce duplicate-content ambiguity.', 'aivisibility-seo-geo-agent'), 'low', 4 ); } else { $checks[] = array( 'category' => 'technical', 'severity' => 'info', 'status' => 'warning', 'finding' => __('Public page source was not checked for this scan.', 'aivisibility-seo-geo-agent'), 'recommendation' => sprintf(__('Allow server-side cURL access to the public page source before running %s recommendations.', 'aivisibility-seo-geo-agent'), AISEO_AUDITOR_NAME), ); } $score = max(0, min(100, $score)); $status_label = $this->score_status_label($score); return array( 'score' => $score, 'summary' => sprintf(__('cURL page-source audit completed with %s status. AI recommendations are used when the AI Engine request succeeds.', 'aivisibility-seo-geo-agent'), $status_label), 'priority_actions' => array_slice($priority_actions, 0, 5), 'checks' => $checks, ); } private function score_status_label($score) { if ($score >= 80) { return __('Green', 'aivisibility-seo-geo-agent'); } if ($score >= 55) { return __('Orange', 'aivisibility-seo-geo-agent'); } return __('Red', 'aivisibility-seo-geo-agent'); } private function local_check(array &$checks, array &$priority_actions, &$score, $category, $pass, $pass_text, $recommendation, $severity, $penalty) { $checks[] = array( 'category' => $category, 'severity' => $pass ? 'info' : $severity, 'status' => $pass ? 'pass' : 'fail', 'finding' => $pass ? $pass_text : __('Needs attention.', 'aivisibility-seo-geo-agent'), 'recommendation' => $pass ? __('No immediate action needed.', 'aivisibility-seo-geo-agent') : $recommendation, ); if (!$pass) { $score -= (int) $penalty; $priority_actions[] = array( 'area' => $category, 'severity' => in_array($severity, array('high', 'medium', 'low'), true) ? $severity : 'medium', 'recommendation' => $recommendation, 'reason' => __('Local cURL/source checks found a weak SEO signal. AI recommendations should be preferred when available.', 'aivisibility-seo-geo-agent'), ); } } private function extract_json_ld_types($decoded_json) { $types = array(); if (!is_array($decoded_json)) { return $types; } if (isset($decoded_json['@type'])) { if (is_array($decoded_json['@type'])) { $types = array_merge($types, array_map('strval', $decoded_json['@type'])); } else { $types[] = (string) $decoded_json['@type']; } } foreach (array('@graph', 'itemListElement') as $key) { if (!empty($decoded_json[$key]) && is_array($decoded_json[$key])) { foreach ($decoded_json[$key] as $item) { $types = array_merge($types, $this->extract_json_ld_types($item)); } } } return $types; } private function add_missing_image_sample(array &$data, DOMElement $img) { if (count($data['images']['samples_missing_alt']) >= 6) { return; } $attachment_id = $this->image_attachment_id($img); $data['images']['samples_missing_alt'][] = array( 'id' => $attachment_id, 'attachment_id' => $attachment_id, 'src' => esc_url_raw($img->getAttribute('src')), 'class' => sanitize_text_field($img->getAttribute('class')), ); } private function add_image_item_sample(array &$data, DOMElement $img, $alt, $has_alt) { if (count($data['images']['items']) >= (int) apply_filters('aiseo_image_inventory_limit', 36)) { return; } $attachment_id = $this->image_attachment_id($img); $src = esc_url_raw($img->getAttribute('src')); if (!$attachment_id && !$src) { return; } $key = $attachment_id ? 'id_' . $attachment_id : 'src_' . md5($src); if (isset($data['images']['items'][$key])) { return; } $data['images']['items'][$key] = array( 'id' => $attachment_id, 'attachment_id' => $attachment_id, 'src' => $src, 'alt' => $this->limit_text((string) $alt, 180), 'has_alt' => (bool) $has_alt, 'class' => sanitize_text_field($img->getAttribute('class')), ); } private function merge_image_items(array $primary, array $secondary) { $merged = array(); foreach (array($primary, $secondary) as $items) { foreach ($items as $item) { if (!is_array($item)) { continue; } $attachment_id = !empty($item['attachment_id']) ? absint($item['attachment_id']) : (!empty($item['id']) ? absint($item['id']) : 0); $src = !empty($item['src']) ? esc_url_raw($item['src']) : ''; if (!$attachment_id && !$src) { continue; } $key = $attachment_id ? 'id_' . $attachment_id : 'src_' . md5($src); if (!isset($merged[$key])) { $item['id'] = $attachment_id; $item['attachment_id'] = $attachment_id; $item['src'] = $src; $merged[$key] = $item; } } } return array_slice(array_values($merged), 0, (int) apply_filters('aiseo_image_inventory_limit', 36)); } private function image_attachment_id(DOMElement $img) { $class = (string) $img->getAttribute('class'); if (preg_match('/(?:^|\s)wp-image-(\d+)(?:\s|$)/', $class, $matches)) { return absint($matches[1]); } $src = esc_url_raw($img->getAttribute('src')); if ($src && function_exists('attachment_url_to_postid')) { return absint(attachment_url_to_postid($src)); } return 0; } private function remove_non_content_nodes(DOMDocument $dom) { foreach (array('script', 'style', 'noscript') as $tag) { $nodes = array(); foreach ($dom->getElementsByTagName($tag) as $node) { $nodes[] = $node; } foreach ($nodes as $node) { if ($node->parentNode) { $node->parentNode->removeChild($node); } } } } private function normalize_text($text) { $text = html_entity_decode((string) $text, ENT_QUOTES, 'UTF-8'); $text = preg_replace('/\s+/u', ' ', $text); return trim($text); } private function limit_text($text, $limit) { $text = $this->normalize_text($text); if ($this->string_length($text) <= $limit) { return $text; } if (function_exists('mb_substr')) { return mb_substr($text, 0, $limit, 'UTF-8') . '...'; } return substr($text, 0, $limit) . '...'; } private function limit_raw_html($html, $limit) { $html = (string) $html; if ($this->string_length($html) <= $limit) { return $html; } if (function_exists('mb_substr')) { return mb_substr($html, 0, $limit, 'UTF-8'); } return substr($html, 0, $limit); } private function word_count($text) { $text = $this->normalize_text($text); if ('' === $text) { return 0; } preg_match_all('/[\p{L}\p{N}]+/u', $text, $matches); return count($matches[0]); } private function string_length($text) { if (function_exists('mb_strlen')) { return mb_strlen((string) $text, 'UTF-8'); } return strlen((string) $text); } }