Changeset 3445423
- Timestamp:
- 01/23/2026 08:57:16 AM (4 weeks ago)
- Location:
- ezy-ai
- Files:
-
- 26 edited
-
tags/1.0.0/admin/connection.php (modified) (12 diffs)
-
tags/1.0.0/ezy-ai-plugin.php (modified) (6 diffs)
-
tags/1.0.0/includes/class-ezy-ai-auth.php (modified) (1 diff)
-
tags/1.0.0/includes/class-ezy-ai-blogs.php (modified) (15 diffs)
-
tags/1.0.0/includes/class-ezy-ai-facts.php (modified) (7 diffs)
-
tags/1.0.0/includes/class-ezy-ai-faqs.php (modified) (3 diffs)
-
tags/1.0.0/includes/class-ezy-ai-llms.php (modified) (9 diffs)
-
tags/1.0.0/includes/class-ezy-ai-meta-descriptions.php (modified) (9 diffs)
-
tags/1.0.0/includes/class-ezy-ai-robots.php (modified) (8 diffs)
-
tags/1.0.0/includes/class-ezy-ai-schema.php (modified) (10 diffs)
-
tags/1.0.0/includes/class-ezy-ai-tracking.php (modified) (11 diffs)
-
tags/1.0.0/includes/class-ezy-ai-webhook.php (modified) (2 diffs)
-
tags/1.0.0/includes/class-ezy-ai-widget-fetch.php (modified) (9 diffs)
-
trunk/admin/connection.php (modified) (12 diffs)
-
trunk/ezy-ai-plugin.php (modified) (6 diffs)
-
trunk/includes/class-ezy-ai-auth.php (modified) (1 diff)
-
trunk/includes/class-ezy-ai-blogs.php (modified) (15 diffs)
-
trunk/includes/class-ezy-ai-facts.php (modified) (7 diffs)
-
trunk/includes/class-ezy-ai-faqs.php (modified) (3 diffs)
-
trunk/includes/class-ezy-ai-llms.php (modified) (9 diffs)
-
trunk/includes/class-ezy-ai-meta-descriptions.php (modified) (9 diffs)
-
trunk/includes/class-ezy-ai-robots.php (modified) (8 diffs)
-
trunk/includes/class-ezy-ai-schema.php (modified) (10 diffs)
-
trunk/includes/class-ezy-ai-tracking.php (modified) (11 diffs)
-
trunk/includes/class-ezy-ai-webhook.php (modified) (2 diffs)
-
trunk/includes/class-ezy-ai-widget-fetch.php (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
ezy-ai/tags/1.0.0/admin/connection.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Connection Settings Page4 *5 * @package EZY_AI6 * @since 1.0.07 */8 2 9 3 if (!defined('ABSPATH')) { … … 31 25 } 32 26 } catch (Exception $e) { 33 // Swallow verification errors silently34 27 } catch (Error $e) { 35 // Swallow verification fatal errors silently36 28 } 37 29 } … … 84 76 } 85 77 86 // Get progress data87 78 global $ezy_ai_webhook; 88 79 if (!$ezy_ai_webhook && $is_connected) { … … 97 88 $ezy_score = $progress_percentage; 98 89 99 // Get site URL for display100 90 $site_url = wp_parse_url(get_site_url(), PHP_URL_HOST); 101 91 102 // Initialize AI visits data - Always show last 7 days103 92 $ai_visits_data = array(); 104 93 $ai_visits_labels = array(); … … 110 99 $chart_title = 'AI visits in the last 7 days'; 111 100 112 // Generate last 7 days labels113 101 $last_7_days_labels = array(); 114 102 for ($i = 6; $i >= 0; $i--) { … … 116 104 } 117 105 118 // Initialize with zeros for all 7 days119 106 $ai_visits_data = array_fill(0, 7, 0); 120 107 $ai_visits_labels = $last_7_days_labels; 121 108 122 // Only fetch data if connected123 109 if ($is_connected) { 124 110 $token = $ezy_ai_auth->get_connection_token(); 125 111 126 112 if ($token) { 127 // Fetch agent stats (total visits, unique agents)128 113 $stats_url = EZY_AI_API_Client::API_BASE_URL . '/integrations/' . $token . '/agent-stats?days=7'; 129 114 $stats_response = wp_remote_get($stats_url, array( … … 145 130 } 146 131 147 // Fetch agent analytics (hourly distribution for chart) - always 7 days148 132 $api_url = EZY_AI_API_Client::API_BASE_URL . '/integrations/' . $token . '/agent-analytics?timeRange=7d'; 149 133 $response = wp_remote_get($api_url, array( … … 160 144 $hourly = $body['data']['hourlyDistribution']; 161 145 162 // Group hourly data by day and map to our 7-day labels163 146 $daily_visits = array(); 164 147 … … 176 159 } 177 160 178 // Map to our preset 7-day labels179 161 foreach ($last_7_days_labels as $index => $label) { 180 162 if (isset($daily_visits[$label])) { … … 187 169 } 188 170 189 // Labels are already set to last 7 days, data defaults to zeros190 191 // Calculate max value for scaling (minimum 1 to avoid division by zero)192 171 $max_visits = max(1, max($ai_visits_data)); 193 172 $num_points = count($ai_visits_data); … … 453 432 $is_active = $i < $active_segments; 454 433 455 // Tapered segments: slim at center, wide at outer edge456 434 $angle_spread_inner = 3.5; 457 435 $angle_spread_outer = 8; … … 459 437 $outer_radius = 92; 460 438 461 // Calculate 4 points for tapered blade shape462 439 $inner_start_angle = deg2rad($angle - $angle_spread_inner); 463 440 $inner_end_angle = deg2rad($angle + $angle_spread_inner); -
ezy-ai/tags/1.0.0/ezy-ai-plugin.php
r3445408 r3445423 35 35 require_once EZY_AI_PLUGIN_DIR . 'includes/class-ezy-ai-blogs.php'; 36 36 require_once EZY_AI_PLUGIN_DIR . 'includes/class-ezy-ai-tracking.php'; 37 38 // Activation and deactivation hooks will be registered at the end of the file39 37 40 38 global $ezy_ai_auth; … … 152 150 private function init_tracking() { 153 151 global $ezy_ai_auth; 154 // Only initialize tracking if connected155 152 if ($ezy_ai_auth && $ezy_ai_auth->is_connected()) { 156 153 new EZY_AI_Tracking(); … … 159 156 160 157 public function add_admin_menu() { 161 // Custom SVG icon - pre-encoded base64, uses fill="black" so WP can recolor it162 // This is the EZY "E" logo icon163 158 $icon_base64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSJibGFjayIgZD0iTTAgMGgxMnY0SDR2NGg4djRINHY0aDEydjRIMHoiLz48cGF0aCBmaWxsPSJibGFjayIgZD0iTTEyIDBoNHY4aDR2NEgxNnY4aC00di04SDh2LTRoNHoiLz48L3N2Zz4='; 164 159 … … 368 363 $ezy_ai_auth->verify_connection(); 369 364 370 // Initialize tracking after successful connection371 365 $this->init_tracking(); 372 366 } elseif ($ezy_ai_auth->get_connection_token()) { … … 482 476 register_deactivation_hook(__FILE__, 'ezy_ai_deactivate'); 483 477 function ezy_ai_deactivate() { 484 // Deactivate component classes485 478 EZY_AI_LLMS::deactivate(); 486 479 EZY_AI_Facts::deactivate(); 487 480 EZY_AI_Robots::deactivate(); 488 481 489 // Unschedule health check cron490 482 $timestamp = wp_next_scheduled('ezy_ai_connection_health_check'); 491 483 if ($timestamp) { … … 532 524 533 525 wp_cache_flush(); 534 535 // swallow debug log536 526 } -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-auth.php
r3440881 r3445423 168 168 delete_transient('ezy_ai_just_connected'); 169 169 170 // swallow debug log171 172 170 return true; 173 171 } -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-blogs.php
r3444216 r3445423 14 14 private $auth; 15 15 16 // allowed tags for full html documents17 16 private function get_allowed_html_tags() 18 17 { … … 49 48 } 50 49 51 // extract ld+json scripts before wp_kses so schema is not stripped, then restore after52 50 private function sanitize_full_html_preserving_ldjson($html) 53 51 { … … 82 80 public function register_blog_routes() 83 81 { 84 // Single blog post: /ezy/blogs/slug85 82 add_rewrite_rule( 86 83 '^ezy/blogs/([^/]+)/?$', … … 89 86 ); 90 87 91 // Blog listing: /ezy/blogs92 88 add_rewrite_rule( 93 89 '^ezy/blogs/?$', … … 107 103 $is_listing = get_query_var('ezy_blog_listing'); 108 104 109 // Handle Listing Page110 105 if ($is_listing) { 111 106 $this->render_blog_listing(); … … 113 108 } 114 109 115 // Handle Single Blog Page116 110 if (!$slug) { 117 111 $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; … … 172 166 private function get_listing_template() 173 167 { 174 // Check cache first175 168 $cached_template = get_transient('ezy_ai_listing_template'); 176 169 if ($cached_template) { … … 178 171 } 179 172 180 // Fetch from API181 173 if (!$this->auth || !$this->auth->is_connected()) { 182 174 return null; … … 192 184 193 185 if (!is_wp_error($response) && !empty($response)) { 194 // Cache for 24 hours195 186 set_transient('ezy_ai_listing_template', $response, 24 * HOUR_IN_SECONDS); 196 187 return $response; 197 188 } 198 189 } catch (Exception $e) { 199 // swallow debug log200 190 } 201 191 … … 208 198 nocache_headers(); 209 199 210 // Enqueue blog styles211 200 wp_enqueue_style( 212 201 'ezy-ai-blogs-style', … … 216 205 ); 217 206 218 // Simple, clean listing page219 207 ?> 220 208 <!DOCTYPE html> … … 332 320 $result = update_option($option_name, $formatted_html, false); 333 321 334 // Update Index with published_at date335 322 $this->update_blog_index($slug, $title, $excerpt, $published_at); 336 323 337 324 if ($result) { 338 // swallow debug log339 340 325 flush_rewrite_rules(false); 341 } else {342 // swallow debug log343 326 } 344 327 … … 350 333 $index = get_option(self::BLOG_INDEX_OPTION, array()); 351 334 352 // Remove existing entry if present353 335 $index = array_filter($index, function ($item) use ($slug) { 354 336 return $item['slug'] !== $slug; 355 337 }); 356 338 357 // Use provided published_at or fallback to current time358 // Convert ISO 8601 format to MySQL datetime if needed359 339 $date = $published_at; 360 340 if ($date) { 361 // Convert ISO 8601 to MySQL datetime format362 341 $timestamp = strtotime($date); 363 342 if ($timestamp !== false) { … … 370 349 } 371 350 372 // Add new entry373 351 $index[] = array( 374 352 'slug' => $slug, … … 410 388 $result = delete_option($option_name); 411 389 412 // Remove from index413 390 $index = get_option(self::BLOG_INDEX_OPTION, array()); 414 391 $index = array_filter($index, function ($item) use ($slug) { -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-facts.php
r3440881 r3445423 24 24 } 25 25 26 /**27 * Setup rewrite rules to ensure facts.json goes through WordPress28 */29 26 public function setup_facts_json_override() { 30 27 add_rewrite_rule('^facts\.json$', 'index.php?ezy_facts_json=1', 'top'); … … 36 33 } 37 34 38 /**39 * Remove any physical facts.json files created by other plugins40 * Only runs if EZY AI has facts.json content configured41 */42 35 public function cleanup_competing_facts_files() { 43 // Only cleanup if we have content to serve44 36 if (!$this->get_facts_json_content()) { 45 37 return; … … 68 60 } 69 61 70 /**71 * Output facts.json content72 * Handles both standard requests and rewritten requests73 */74 62 public function output_facts_json() { 75 63 if (get_query_var('ezy_facts_json')) { … … 88 76 } 89 77 90 /**91 * Actually serve the facts.json content92 */93 78 private function serve_facts_json() { 94 79 $facts_content = $this->get_facts_json_content(); … … 172 157 } 173 158 174 /**175 * Flush rewrite rules if needed176 */177 159 private function maybe_flush_rewrite_rules() { 178 160 $rules = get_option('rewrite_rules'); … … 182 164 } 183 165 184 /**185 * Static method to flush rewrite rules on plugin activation186 */187 166 public static function activate() { 188 167 add_rewrite_rule('^facts\.json$', 'index.php?ezy_facts_json=1', 'top'); … … 190 169 } 191 170 192 /**193 * Static method to clean up on plugin deactivation194 */195 171 public static function deactivate() { 196 172 flush_rewrite_rules(false); -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-faqs.php
r3440881 r3445423 42 42 nocache_headers(); 43 43 44 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped45 44 echo $faqs_content; 46 45 … … 59 58 } 60 59 61 // Return content even if empty (for clearing)62 60 if (isset($response['content'])) { 63 61 return $response['content']; … … 79 77 $content = $this->fetch_faqs_html_from_api($token); 80 78 81 // If content is explicitly empty string, clear FAQs82 79 if ($content === '') { 83 80 return $this->clear_faqs_html(); 84 81 } 85 82 86 // If content exists, update it87 83 if ($content) { 88 84 return $this->update_faqs_html($content); -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-llms.php
r3445412 r3445423 29 29 } 30 30 31 /**32 * Block Yoast SEO from creating llms.txt file33 * Returns a non-writable path so Yoast fails silently34 *35 * @param string $path Original path from Yoast36 * @return string Modified path that prevents file creation37 */38 31 public function block_yoast_llms_file($path) { 39 32 if ($this->get_llms_txt_content()) { … … 43 36 } 44 37 45 /**46 * Setup rewrite rules to ensure llms.txt goes through WordPress47 */48 38 public function setup_llms_txt_override() { 49 39 add_rewrite_rule('^llms\.txt$', 'index.php?ezy_llms_txt=1', 'top'); … … 56 46 } 57 47 58 /**59 * Remove any physical llms.txt files created by other plugins (like Yoast)60 * Only runs if EZY AI has llms.txt content configured61 */62 48 public function cleanup_competing_llms_files() { 63 49 if (!$this->get_llms_txt_content()) { … … 97 83 } 98 84 99 /**100 * Detect if site is hosted on WP Engine101 */102 85 private function is_wp_engine() { 103 86 return (defined('WPE_APIKEY') || isset($_SERVER['IS_WPE']) || isset($_SERVER['IS_WPE_SNAPSHOT'])); 104 87 } 105 88 106 /**107 * Output llms.txt content108 * Handles both standard requests and rewritten requests109 * Redirects to trailing slash on WP Engine to bypass Nginx blocking110 */111 89 public function output_llms_txt() { 112 90 if (get_query_var('ezy_llms_txt')) { … … 143 121 } 144 122 145 /**146 * Actually serve the llms.txt content147 */148 123 private function serve_llms_txt() { 149 124 $llms_content = $this->get_llms_txt_content(); … … 167 142 } 168 143 169 /**170 * Actually serve the llms-full.txt content171 */172 144 private function serve_llms_full_txt() { 173 145 $llms_full_content = $this->get_llms_full_txt_content(); … … 278 250 } 279 251 280 /**281 * Flush rewrite rules if needed282 * Only flushes if our rule doesn't exist yet283 */284 252 private function maybe_flush_rewrite_rules() { 285 253 $rules = get_option('rewrite_rules'); … … 289 257 } 290 258 291 /**292 * Static method to flush rewrite rules on plugin activation293 */294 259 public static function activate() { 295 260 add_rewrite_rule('^llms\.txt$', 'index.php?ezy_llms_txt=1', 'top'); … … 298 263 } 299 264 300 /**301 * Static method to clean up on plugin deactivation302 */303 265 public static function deactivate() { 304 266 flush_rewrite_rules(false); -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-meta-descriptions.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Meta Descriptions Handler4 *5 * Handles meta description injection with guaranteed priority over Yoast SEO.6 * Uses multiple strategies to ensure EZY meta descriptions take precedence.7 */8 2 9 3 if (!defined('ABSPATH')) { … … 19 13 private $auth; 20 14 21 /**22 * Cache for current page meta description to avoid repeated lookups23 */24 15 private $current_page_meta_cache = null; 25 16 26 /**27 * Flag to track if we've already determined meta for this request28 */29 17 private $meta_checked = false; 30 18 … … 38 26 } 39 27 40 /**41 * Inject EZY meta description into wp_head42 * Runs at priority 1 to appear before most other meta tags43 */44 28 public function inject_meta_description() { 45 29 $meta_description = $this->get_ezy_meta_for_current_page(); … … 53 37 } 54 38 55 /**56 * Comprehensive Yoast meta description blocking57 * Uses multiple filters and high priority to ensure EZY meta descriptions take precedence58 */59 39 private function disable_yoast_meta_interference() { 60 40 add_filter('wpseo_metadesc', array($this, 'override_yoast_meta_description'), 1, 2); … … 67 47 } 68 48 69 /**70 * Get cached EZY meta description for current page71 */72 49 private function get_ezy_meta_for_current_page() { 73 50 if ($this->meta_checked) { … … 89 66 } 90 67 91 /**92 * Check if EZY has meta description for current page93 */94 68 private function has_ezy_meta_for_current_page() { 95 69 return !empty($this->get_ezy_meta_for_current_page()); 96 70 } 97 71 98 /**99 * Primary override: Replace Yoast's meta description with EZY's100 */101 72 public function override_yoast_meta_description($description, $presentation = null) { 102 73 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 109 80 } 110 81 111 /**112 * Late override: Safety net to ensure EZY meta description is used113 */114 82 public function override_yoast_meta_description_late($description, $presentation = null) { 115 83 return $this->override_yoast_meta_description($description, $presentation); 116 84 } 117 85 118 /**119 * Override Yoast's Open Graph description120 */121 86 public function override_yoast_og_description($description, $presentation = null) { 122 87 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 129 94 } 130 95 131 /**132 * Late override for Open Graph description133 */134 96 public function override_yoast_og_description_late($description, $presentation = null) { 135 97 return $this->override_yoast_og_description($description, $presentation); 136 98 } 137 99 138 /**139 * Override Yoast's Twitter description140 */141 100 public function override_yoast_twitter_description($description, $presentation = null) { 142 101 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 149 108 } 150 109 151 /**152 * Late override for Twitter description153 */154 110 public function override_yoast_twitter_description_late($description, $presentation = null) { 155 111 return $this->override_yoast_twitter_description($description, $presentation); 156 112 } 157 113 158 /**159 * Override Yoast's frontend presentation object160 * This ensures the meta description is set at the presentation level161 */162 114 public function override_yoast_presentation($presentation, $context = null) { 163 115 $ezy_meta = $this->get_ezy_meta_for_current_page(); -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-robots.php
r3445411 r3445423 27 27 } 28 28 29 /**30 * Block Yoast SEO's robots.txt modifications when EZY has content31 * This runs early (priority 1) to capture the original robots.txt32 * before Yoast modifies it33 *34 * @param string $output The robots.txt output35 * @param bool $public Whether the site is public36 * @return string The robots.txt output37 */38 29 public function block_yoast_robots_output($output, $public) { 39 30 return $output; 40 31 } 41 32 42 /**43 * Filter the robots.txt output - runs AFTER Yoast44 * Completely replaces content with EZY's robots.txt if we have content45 *46 * @param string $output The robots.txt output (possibly modified by Yoast)47 * @param bool $public Whether the site is public48 * @return string The filtered robots.txt output49 */50 33 public function filter_robots_txt($output, $public) { 51 34 $robots_content = $this->get_robots_txt_content(); … … 57 40 } 58 41 59 /**60 * Output robots.txt content via do_robots action61 * This is a fallback that runs before WordPress's default output62 */63 42 public function output_robots_txt() { 64 43 $robots_content = $this->get_robots_txt_content(); … … 77 56 } 78 57 79 /**80 * Setup rewrite rule for robots.txt to handle physical file override (WP Engine fix)81 */82 58 public function setup_robots_txt_rewrite() { 83 59 add_rewrite_rule('^robots\.txt$', 'index.php?ezy_robots_txt=1', 'top'); 84 60 } 85 61 86 /**87 * Add custom query var for robots.txt rewrite88 */89 62 public function add_robots_query_var($vars) { 90 63 $vars[] = 'ezy_robots_txt'; … … 92 65 } 93 66 94 /**95 * Serve robots.txt via rewrite rule (bypasses physical file)96 */97 67 public function serve_robots_via_rewrite() { 98 68 if (!get_query_var('ezy_robots_txt')) { … … 121 91 } 122 92 123 /**124 * Remove any physical robots.txt files when EZY has content125 * Note: Only removes files that appear to be auto-generated (not manually created)126 */127 93 public function cleanup_competing_robots_files() { 128 94 if (!$this->get_robots_txt_content()) { … … 147 113 foreach ($paths_to_check as $file_path) { 148 114 if (file_exists($file_path) && wp_is_writable($file_path)) { 149 // Check if this is a Yoast-generated file or generic auto-generated150 115 $content = @file_get_contents($file_path); 151 116 if ($content !== false) { 152 // Check for Yoast signature or generic WordPress robots153 117 $is_auto_generated = ( 154 118 strpos($content, 'Yoast') !== false || 155 119 strpos($content, '# START YOAST BLOCK') !== false || 156 // Generic WordPress default robots.txt pattern157 120 (strpos($content, 'User-agent: *') !== false && 158 121 strpos($content, 'Disallow: /wp-admin/') !== false && … … 223 186 } 224 187 225 /**226 * Static method for plugin activation227 */228 188 public static function activate() { 229 189 add_rewrite_rule('^robots\.txt$', 'index.php?ezy_robots_txt=1', 'top'); … … 231 191 } 232 192 233 /**234 * Static method for plugin deactivation235 */236 193 public static function deactivate() { 237 // Nothing special needed for robots.txt238 194 } 239 195 -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-schema.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Schema.org Markup Handler4 *5 * Handles schema.org JSON-LD injection with guaranteed priority over Yoast SEO.6 * Uses multiple strategies to ensure EZY schemas take precedence.7 */8 2 9 3 if (!defined('ABSPATH')) { … … 20 14 private $auth; 21 15 22 /**23 * Cache for current page schemas to avoid repeated lookups24 */25 16 private $current_page_schemas_cache = null; 26 17 27 /**28 * Flag to track if we've already determined schemas for this request29 */30 18 private $schemas_checked = false; 31 19 … … 38 26 } 39 27 40 /**41 * Comprehensive Yoast schema blocking42 * Uses multiple filters and strategies to ensure EZY schemas take priority43 */44 28 private function disable_yoast_schema_interference() { 45 29 add_filter('wpseo_json_ld_output', array($this, 'block_yoast_jsonld_output'), 1, 1); … … 58 42 } 59 43 60 /**61 * Primary blocking method: Return FALSE to completely disable Yoast's JSON-LD output62 * This is the most effective method based on Yoast's schema-presenter.php63 */64 44 public function block_yoast_jsonld_output($data) { 65 45 if ($this->has_ezy_schemas_for_current_page()) { … … 70 50 } 71 51 72 /**73 * Secondary blocking method: Clear the schema graph74 */75 52 public function block_yoast_schema_graph($graph, $context) { 76 53 if ($this->has_ezy_schemas_for_current_page()) { … … 81 58 } 82 59 83 /**84 * Block individual schema pieces when EZY has schemas for the page85 */86 60 public function block_yoast_schema_piece($is_needed) { 87 61 if ($this->has_ezy_schemas_for_current_page()) { … … 92 66 } 93 67 94 /**95 * Block the entire graph pieces array96 */97 68 public function block_yoast_graph_pieces($pieces, $context) { 98 69 if ($this->has_ezy_schemas_for_current_page()) { … … 103 74 } 104 75 105 /**106 * Check if EZY has schemas for the current page107 * Uses caching to avoid repeated database lookups108 */109 76 private function has_ezy_schemas_for_current_page() { 110 77 if ($this->schemas_checked) { … … 126 93 } 127 94 128 /**129 * Inject EZY schema markup into wp_head130 * Runs at priority 1 to appear before most other scripts131 */132 95 public function inject_schema_markup() { 133 96 if (!$this->schemas_checked) { … … 239 202 } 240 203 241 // Use wp_print_inline_script_tag for safe JSON-LD output (WordPress 5.7+)242 // This function handles escaping internally and is the recommended approach243 204 echo "\n<!-- EZY.AI Schema.org Markup -->\n"; 244 205 wp_print_inline_script_tag( -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-tracking.php
r3440881 r3445423 17 17 $this->auth = $ezy_ai_auth; 18 18 19 // Hook into WordPress request lifecycle20 // Use template_redirect for frontend requests only21 19 add_action('template_redirect', array($this, 'track_visit'), 1); 22 20 } … … 24 22 public function track_visit() 25 23 { 26 // Only track if connected27 24 if (!$this->auth || !$this->auth->is_connected()) { 28 25 return; 29 26 } 30 27 31 // Skip tracking for admin, AJAX, cron, and REST API requests32 28 if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST)) { 33 29 return; 34 30 } 35 31 36 // Skip tracking for non-GET requests (except HEAD)37 32 $method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET'; 38 33 if ($method !== 'GET' && $method !== 'HEAD') { … … 40 35 } 41 36 42 // Get connection token43 37 $token = $this->auth->get_connection_token(); 44 38 if (!$token) { … … 46 40 } 47 41 48 // Extract visit data49 42 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; 50 43 $ip_address = $this->get_client_ip(); 51 44 $url = $this->get_current_url(); 52 45 53 // Extract all headers for signature verification54 46 $headers = $this->get_all_headers(); 55 47 56 // Send tracking request asynchronously (non-blocking)57 48 $this->send_tracking_request($token, $user_agent, $ip_address, $url, $headers); 58 49 } … … 72 63 $ip = sanitize_text_field(wp_unslash($_SERVER[$key])); 73 64 74 // Handle comma-separated IPs (X-Forwarded-For)75 65 if (strpos($ip, ',') !== false) { 76 66 $ips = explode(',', $ip); … … 78 68 } 79 69 80 // Validate IP address81 70 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { 82 71 return $ip; … … 121 110 ); 122 111 123 // Get all HTTP headers124 112 if (function_exists('getallheaders')) { 125 113 $all_headers = getallheaders(); … … 132 120 } 133 121 } else { 134 // Fallback for servers without getallheaders()135 122 foreach ($_SERVER as $key => $value) { 136 123 if (strpos($key, 'HTTP_') === 0) { … … 143 130 } 144 131 145 // Ensure signature headers are included (case-insensitive)146 132 $signature_headers = array('signature-agent', 'signature-input', 'signature'); 147 133 foreach ($signature_headers as $sig_header) { … … 157 143 private function send_tracking_request($token, $user_agent, $ip_address, $url, $headers) 158 144 { 159 // Use wp_remote_post for async request (non-blocking)160 145 $api_url = EZY_AI_API_Client::API_BASE_URL . '/track'; 161 146 … … 174 159 )), 175 160 'timeout' => 5, 176 'blocking' => false, // Non-blocking request161 'blocking' => false, 177 162 'sslverify' => true, 178 163 ); 179 164 180 // Fire and forget - don't wait for response181 165 wp_remote_post($api_url, $request_args); 182 166 } -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-webhook.php
r3444216 r3445423 123 123 $updated_at = $request->get_param('updated_at'); 124 124 125 // swallow debug log126 127 125 $result = false; 128 126 … … 308 306 } 309 307 310 // If content is explicitly empty string, clear FAQs311 308 if ($content === '') { 312 309 return $ezy_ai_faqs->clear_faqs_html(); 313 310 } 314 311 315 // If content is null or false, return false316 312 if (empty($content)) { 317 313 return false; -
ezy-ai/tags/1.0.0/includes/class-ezy-ai-widget-fetch.php
r3440881 r3445423 124 124 125 125 private function fetch_robots() { 126 // Always fetch live robots.txt from the website127 126 $site_url = get_site_url(); 128 127 $robots_url = rtrim($site_url, '/') . '/robots.txt'; … … 155 154 156 155 private function fetch_llms() { 157 // Always fetch live llms.txt from the website158 156 $site_url = get_site_url(); 159 157 $llms_url = rtrim($site_url, '/') . '/llms.txt'; … … 204 202 205 203 private function fetch_schema($page_urls = null) { 206 // Always fetch live schemas from pages207 204 $live_schemas = array(); 208 205 $total_pages_requested = 0; … … 239 236 240 237 private function fetch_meta($page_urls = null) { 241 // Return ALL cached meta descriptions - let the backend do the filtering242 // This is more reliable than trying to match URLs in WordPress243 238 global $ezy_ai_meta_descriptions; 244 239 if (!$ezy_ai_meta_descriptions) { … … 246 241 } 247 242 248 // Get all cached meta descriptions249 243 $all_cached = get_option(EZY_AI_Meta_Descriptions::META_DESCRIPTIONS_OPTION); 250 244 … … 277 271 278 272 private function fetch_sitemap() { 279 // Always fetch live sitemap.xml from the website280 273 $site_url = get_site_url(); 281 274 $sitemap_url = rtrim($site_url, '/') . '/sitemap.xml'; … … 316 309 $updated_at = get_option('ezy_ai_facts_json_updated_at'); 317 310 318 // If no stored content, try to fetch live facts.json from the website319 311 if (empty($content)) { 320 312 $site_url = get_site_url(); … … 400 392 $schemas = array(); 401 393 402 // Extract JSON-LD schemas from <script type="application/ld+json">403 394 preg_match_all('/<script[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $html, $matches); 404 395 … … 431 422 } 432 423 433 // Extract meta description from <meta name="description">434 424 $meta_description = ''; 435 425 $is_ezy_generated = false; 436 426 437 // Try to find meta description tag438 427 if (preg_match('/<meta[^>]*name=["\']description["\'][^>]*>/i', $html, $matches, PREG_OFFSET_CAPTURE)) { 439 428 $full_match = $matches[0][0]; 440 429 $match_index = $matches[0][1]; 441 430 442 // Extract content value443 431 if (preg_match('/content=["\']([^"\']*)["\']/i', $full_match, $content_matches)) { 444 432 $meta_description = html_entity_decode($content_matches[1], ENT_QUOTES, 'UTF-8'); 445 433 446 // Check for data-ezy-generated attribute447 434 if (preg_match('/data-ezy-generated\s*=\s*["\']true["\']/i', $full_match)) { 448 435 $is_ezy_generated = true; 449 436 } else { 450 // Check for EZY.AI comment within 200 chars before or after451 437 $before_context = substr($html, max(0, $match_index - 200), min(200, $match_index)); 452 438 $after_context = substr($html, $match_index + strlen($full_match), 200); -
ezy-ai/trunk/admin/connection.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Connection Settings Page4 *5 * @package EZY_AI6 * @since 1.0.07 */8 2 9 3 if (!defined('ABSPATH')) { … … 31 25 } 32 26 } catch (Exception $e) { 33 // Swallow verification errors silently34 27 } catch (Error $e) { 35 // Swallow verification fatal errors silently36 28 } 37 29 } … … 84 76 } 85 77 86 // Get progress data87 78 global $ezy_ai_webhook; 88 79 if (!$ezy_ai_webhook && $is_connected) { … … 97 88 $ezy_score = $progress_percentage; 98 89 99 // Get site URL for display100 90 $site_url = wp_parse_url(get_site_url(), PHP_URL_HOST); 101 91 102 // Initialize AI visits data - Always show last 7 days103 92 $ai_visits_data = array(); 104 93 $ai_visits_labels = array(); … … 110 99 $chart_title = 'AI visits in the last 7 days'; 111 100 112 // Generate last 7 days labels113 101 $last_7_days_labels = array(); 114 102 for ($i = 6; $i >= 0; $i--) { … … 116 104 } 117 105 118 // Initialize with zeros for all 7 days119 106 $ai_visits_data = array_fill(0, 7, 0); 120 107 $ai_visits_labels = $last_7_days_labels; 121 108 122 // Only fetch data if connected123 109 if ($is_connected) { 124 110 $token = $ezy_ai_auth->get_connection_token(); 125 111 126 112 if ($token) { 127 // Fetch agent stats (total visits, unique agents)128 113 $stats_url = EZY_AI_API_Client::API_BASE_URL . '/integrations/' . $token . '/agent-stats?days=7'; 129 114 $stats_response = wp_remote_get($stats_url, array( … … 145 130 } 146 131 147 // Fetch agent analytics (hourly distribution for chart) - always 7 days148 132 $api_url = EZY_AI_API_Client::API_BASE_URL . '/integrations/' . $token . '/agent-analytics?timeRange=7d'; 149 133 $response = wp_remote_get($api_url, array( … … 160 144 $hourly = $body['data']['hourlyDistribution']; 161 145 162 // Group hourly data by day and map to our 7-day labels163 146 $daily_visits = array(); 164 147 … … 176 159 } 177 160 178 // Map to our preset 7-day labels179 161 foreach ($last_7_days_labels as $index => $label) { 180 162 if (isset($daily_visits[$label])) { … … 187 169 } 188 170 189 // Labels are already set to last 7 days, data defaults to zeros190 191 // Calculate max value for scaling (minimum 1 to avoid division by zero)192 171 $max_visits = max(1, max($ai_visits_data)); 193 172 $num_points = count($ai_visits_data); … … 453 432 $is_active = $i < $active_segments; 454 433 455 // Tapered segments: slim at center, wide at outer edge456 434 $angle_spread_inner = 3.5; 457 435 $angle_spread_outer = 8; … … 459 437 $outer_radius = 92; 460 438 461 // Calculate 4 points for tapered blade shape462 439 $inner_start_angle = deg2rad($angle - $angle_spread_inner); 463 440 $inner_end_angle = deg2rad($angle + $angle_spread_inner); -
ezy-ai/trunk/ezy-ai-plugin.php
r3445408 r3445423 35 35 require_once EZY_AI_PLUGIN_DIR . 'includes/class-ezy-ai-blogs.php'; 36 36 require_once EZY_AI_PLUGIN_DIR . 'includes/class-ezy-ai-tracking.php'; 37 38 // Activation and deactivation hooks will be registered at the end of the file39 37 40 38 global $ezy_ai_auth; … … 152 150 private function init_tracking() { 153 151 global $ezy_ai_auth; 154 // Only initialize tracking if connected155 152 if ($ezy_ai_auth && $ezy_ai_auth->is_connected()) { 156 153 new EZY_AI_Tracking(); … … 159 156 160 157 public function add_admin_menu() { 161 // Custom SVG icon - pre-encoded base64, uses fill="black" so WP can recolor it162 // This is the EZY "E" logo icon163 158 $icon_base64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSJibGFjayIgZD0iTTAgMGgxMnY0SDR2NGg4djRINHY0aDEydjRIMHoiLz48cGF0aCBmaWxsPSJibGFjayIgZD0iTTEyIDBoNHY4aDR2NEgxNnY4aC00di04SDh2LTRoNHoiLz48L3N2Zz4='; 164 159 … … 368 363 $ezy_ai_auth->verify_connection(); 369 364 370 // Initialize tracking after successful connection371 365 $this->init_tracking(); 372 366 } elseif ($ezy_ai_auth->get_connection_token()) { … … 482 476 register_deactivation_hook(__FILE__, 'ezy_ai_deactivate'); 483 477 function ezy_ai_deactivate() { 484 // Deactivate component classes485 478 EZY_AI_LLMS::deactivate(); 486 479 EZY_AI_Facts::deactivate(); 487 480 EZY_AI_Robots::deactivate(); 488 481 489 // Unschedule health check cron490 482 $timestamp = wp_next_scheduled('ezy_ai_connection_health_check'); 491 483 if ($timestamp) { … … 532 524 533 525 wp_cache_flush(); 534 535 // swallow debug log536 526 } -
ezy-ai/trunk/includes/class-ezy-ai-auth.php
r3440881 r3445423 168 168 delete_transient('ezy_ai_just_connected'); 169 169 170 // swallow debug log171 172 170 return true; 173 171 } -
ezy-ai/trunk/includes/class-ezy-ai-blogs.php
r3444216 r3445423 14 14 private $auth; 15 15 16 // allowed tags for full html documents17 16 private function get_allowed_html_tags() 18 17 { … … 49 48 } 50 49 51 // extract ld+json scripts before wp_kses so schema is not stripped, then restore after52 50 private function sanitize_full_html_preserving_ldjson($html) 53 51 { … … 82 80 public function register_blog_routes() 83 81 { 84 // Single blog post: /ezy/blogs/slug85 82 add_rewrite_rule( 86 83 '^ezy/blogs/([^/]+)/?$', … … 89 86 ); 90 87 91 // Blog listing: /ezy/blogs92 88 add_rewrite_rule( 93 89 '^ezy/blogs/?$', … … 107 103 $is_listing = get_query_var('ezy_blog_listing'); 108 104 109 // Handle Listing Page110 105 if ($is_listing) { 111 106 $this->render_blog_listing(); … … 113 108 } 114 109 115 // Handle Single Blog Page116 110 if (!$slug) { 117 111 $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; … … 172 166 private function get_listing_template() 173 167 { 174 // Check cache first175 168 $cached_template = get_transient('ezy_ai_listing_template'); 176 169 if ($cached_template) { … … 178 171 } 179 172 180 // Fetch from API181 173 if (!$this->auth || !$this->auth->is_connected()) { 182 174 return null; … … 192 184 193 185 if (!is_wp_error($response) && !empty($response)) { 194 // Cache for 24 hours195 186 set_transient('ezy_ai_listing_template', $response, 24 * HOUR_IN_SECONDS); 196 187 return $response; 197 188 } 198 189 } catch (Exception $e) { 199 // swallow debug log200 190 } 201 191 … … 208 198 nocache_headers(); 209 199 210 // Enqueue blog styles211 200 wp_enqueue_style( 212 201 'ezy-ai-blogs-style', … … 216 205 ); 217 206 218 // Simple, clean listing page219 207 ?> 220 208 <!DOCTYPE html> … … 332 320 $result = update_option($option_name, $formatted_html, false); 333 321 334 // Update Index with published_at date335 322 $this->update_blog_index($slug, $title, $excerpt, $published_at); 336 323 337 324 if ($result) { 338 // swallow debug log339 340 325 flush_rewrite_rules(false); 341 } else {342 // swallow debug log343 326 } 344 327 … … 350 333 $index = get_option(self::BLOG_INDEX_OPTION, array()); 351 334 352 // Remove existing entry if present353 335 $index = array_filter($index, function ($item) use ($slug) { 354 336 return $item['slug'] !== $slug; 355 337 }); 356 338 357 // Use provided published_at or fallback to current time358 // Convert ISO 8601 format to MySQL datetime if needed359 339 $date = $published_at; 360 340 if ($date) { 361 // Convert ISO 8601 to MySQL datetime format362 341 $timestamp = strtotime($date); 363 342 if ($timestamp !== false) { … … 370 349 } 371 350 372 // Add new entry373 351 $index[] = array( 374 352 'slug' => $slug, … … 410 388 $result = delete_option($option_name); 411 389 412 // Remove from index413 390 $index = get_option(self::BLOG_INDEX_OPTION, array()); 414 391 $index = array_filter($index, function ($item) use ($slug) { -
ezy-ai/trunk/includes/class-ezy-ai-facts.php
r3440881 r3445423 24 24 } 25 25 26 /**27 * Setup rewrite rules to ensure facts.json goes through WordPress28 */29 26 public function setup_facts_json_override() { 30 27 add_rewrite_rule('^facts\.json$', 'index.php?ezy_facts_json=1', 'top'); … … 36 33 } 37 34 38 /**39 * Remove any physical facts.json files created by other plugins40 * Only runs if EZY AI has facts.json content configured41 */42 35 public function cleanup_competing_facts_files() { 43 // Only cleanup if we have content to serve44 36 if (!$this->get_facts_json_content()) { 45 37 return; … … 68 60 } 69 61 70 /**71 * Output facts.json content72 * Handles both standard requests and rewritten requests73 */74 62 public function output_facts_json() { 75 63 if (get_query_var('ezy_facts_json')) { … … 88 76 } 89 77 90 /**91 * Actually serve the facts.json content92 */93 78 private function serve_facts_json() { 94 79 $facts_content = $this->get_facts_json_content(); … … 172 157 } 173 158 174 /**175 * Flush rewrite rules if needed176 */177 159 private function maybe_flush_rewrite_rules() { 178 160 $rules = get_option('rewrite_rules'); … … 182 164 } 183 165 184 /**185 * Static method to flush rewrite rules on plugin activation186 */187 166 public static function activate() { 188 167 add_rewrite_rule('^facts\.json$', 'index.php?ezy_facts_json=1', 'top'); … … 190 169 } 191 170 192 /**193 * Static method to clean up on plugin deactivation194 */195 171 public static function deactivate() { 196 172 flush_rewrite_rules(false); -
ezy-ai/trunk/includes/class-ezy-ai-faqs.php
r3440881 r3445423 42 42 nocache_headers(); 43 43 44 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped45 44 echo $faqs_content; 46 45 … … 59 58 } 60 59 61 // Return content even if empty (for clearing)62 60 if (isset($response['content'])) { 63 61 return $response['content']; … … 79 77 $content = $this->fetch_faqs_html_from_api($token); 80 78 81 // If content is explicitly empty string, clear FAQs82 79 if ($content === '') { 83 80 return $this->clear_faqs_html(); 84 81 } 85 82 86 // If content exists, update it87 83 if ($content) { 88 84 return $this->update_faqs_html($content); -
ezy-ai/trunk/includes/class-ezy-ai-llms.php
r3445412 r3445423 29 29 } 30 30 31 /**32 * Block Yoast SEO from creating llms.txt file33 * Returns a non-writable path so Yoast fails silently34 *35 * @param string $path Original path from Yoast36 * @return string Modified path that prevents file creation37 */38 31 public function block_yoast_llms_file($path) { 39 32 if ($this->get_llms_txt_content()) { … … 43 36 } 44 37 45 /**46 * Setup rewrite rules to ensure llms.txt goes through WordPress47 */48 38 public function setup_llms_txt_override() { 49 39 add_rewrite_rule('^llms\.txt$', 'index.php?ezy_llms_txt=1', 'top'); … … 56 46 } 57 47 58 /**59 * Remove any physical llms.txt files created by other plugins (like Yoast)60 * Only runs if EZY AI has llms.txt content configured61 */62 48 public function cleanup_competing_llms_files() { 63 49 if (!$this->get_llms_txt_content()) { … … 97 83 } 98 84 99 /**100 * Detect if site is hosted on WP Engine101 */102 85 private function is_wp_engine() { 103 86 return (defined('WPE_APIKEY') || isset($_SERVER['IS_WPE']) || isset($_SERVER['IS_WPE_SNAPSHOT'])); 104 87 } 105 88 106 /**107 * Output llms.txt content108 * Handles both standard requests and rewritten requests109 * Redirects to trailing slash on WP Engine to bypass Nginx blocking110 */111 89 public function output_llms_txt() { 112 90 if (get_query_var('ezy_llms_txt')) { … … 143 121 } 144 122 145 /**146 * Actually serve the llms.txt content147 */148 123 private function serve_llms_txt() { 149 124 $llms_content = $this->get_llms_txt_content(); … … 167 142 } 168 143 169 /**170 * Actually serve the llms-full.txt content171 */172 144 private function serve_llms_full_txt() { 173 145 $llms_full_content = $this->get_llms_full_txt_content(); … … 278 250 } 279 251 280 /**281 * Flush rewrite rules if needed282 * Only flushes if our rule doesn't exist yet283 */284 252 private function maybe_flush_rewrite_rules() { 285 253 $rules = get_option('rewrite_rules'); … … 289 257 } 290 258 291 /**292 * Static method to flush rewrite rules on plugin activation293 */294 259 public static function activate() { 295 260 add_rewrite_rule('^llms\.txt$', 'index.php?ezy_llms_txt=1', 'top'); … … 298 263 } 299 264 300 /**301 * Static method to clean up on plugin deactivation302 */303 265 public static function deactivate() { 304 266 flush_rewrite_rules(false); -
ezy-ai/trunk/includes/class-ezy-ai-meta-descriptions.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Meta Descriptions Handler4 *5 * Handles meta description injection with guaranteed priority over Yoast SEO.6 * Uses multiple strategies to ensure EZY meta descriptions take precedence.7 */8 2 9 3 if (!defined('ABSPATH')) { … … 19 13 private $auth; 20 14 21 /**22 * Cache for current page meta description to avoid repeated lookups23 */24 15 private $current_page_meta_cache = null; 25 16 26 /**27 * Flag to track if we've already determined meta for this request28 */29 17 private $meta_checked = false; 30 18 … … 38 26 } 39 27 40 /**41 * Inject EZY meta description into wp_head42 * Runs at priority 1 to appear before most other meta tags43 */44 28 public function inject_meta_description() { 45 29 $meta_description = $this->get_ezy_meta_for_current_page(); … … 53 37 } 54 38 55 /**56 * Comprehensive Yoast meta description blocking57 * Uses multiple filters and high priority to ensure EZY meta descriptions take precedence58 */59 39 private function disable_yoast_meta_interference() { 60 40 add_filter('wpseo_metadesc', array($this, 'override_yoast_meta_description'), 1, 2); … … 67 47 } 68 48 69 /**70 * Get cached EZY meta description for current page71 */72 49 private function get_ezy_meta_for_current_page() { 73 50 if ($this->meta_checked) { … … 89 66 } 90 67 91 /**92 * Check if EZY has meta description for current page93 */94 68 private function has_ezy_meta_for_current_page() { 95 69 return !empty($this->get_ezy_meta_for_current_page()); 96 70 } 97 71 98 /**99 * Primary override: Replace Yoast's meta description with EZY's100 */101 72 public function override_yoast_meta_description($description, $presentation = null) { 102 73 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 109 80 } 110 81 111 /**112 * Late override: Safety net to ensure EZY meta description is used113 */114 82 public function override_yoast_meta_description_late($description, $presentation = null) { 115 83 return $this->override_yoast_meta_description($description, $presentation); 116 84 } 117 85 118 /**119 * Override Yoast's Open Graph description120 */121 86 public function override_yoast_og_description($description, $presentation = null) { 122 87 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 129 94 } 130 95 131 /**132 * Late override for Open Graph description133 */134 96 public function override_yoast_og_description_late($description, $presentation = null) { 135 97 return $this->override_yoast_og_description($description, $presentation); 136 98 } 137 99 138 /**139 * Override Yoast's Twitter description140 */141 100 public function override_yoast_twitter_description($description, $presentation = null) { 142 101 $ezy_meta = $this->get_ezy_meta_for_current_page(); … … 149 108 } 150 109 151 /**152 * Late override for Twitter description153 */154 110 public function override_yoast_twitter_description_late($description, $presentation = null) { 155 111 return $this->override_yoast_twitter_description($description, $presentation); 156 112 } 157 113 158 /**159 * Override Yoast's frontend presentation object160 * This ensures the meta description is set at the presentation level161 */162 114 public function override_yoast_presentation($presentation, $context = null) { 163 115 $ezy_meta = $this->get_ezy_meta_for_current_page(); -
ezy-ai/trunk/includes/class-ezy-ai-robots.php
r3445411 r3445423 27 27 } 28 28 29 /**30 * Block Yoast SEO's robots.txt modifications when EZY has content31 * This runs early (priority 1) to capture the original robots.txt32 * before Yoast modifies it33 *34 * @param string $output The robots.txt output35 * @param bool $public Whether the site is public36 * @return string The robots.txt output37 */38 29 public function block_yoast_robots_output($output, $public) { 39 30 return $output; 40 31 } 41 32 42 /**43 * Filter the robots.txt output - runs AFTER Yoast44 * Completely replaces content with EZY's robots.txt if we have content45 *46 * @param string $output The robots.txt output (possibly modified by Yoast)47 * @param bool $public Whether the site is public48 * @return string The filtered robots.txt output49 */50 33 public function filter_robots_txt($output, $public) { 51 34 $robots_content = $this->get_robots_txt_content(); … … 57 40 } 58 41 59 /**60 * Output robots.txt content via do_robots action61 * This is a fallback that runs before WordPress's default output62 */63 42 public function output_robots_txt() { 64 43 $robots_content = $this->get_robots_txt_content(); … … 77 56 } 78 57 79 /**80 * Setup rewrite rule for robots.txt to handle physical file override (WP Engine fix)81 */82 58 public function setup_robots_txt_rewrite() { 83 59 add_rewrite_rule('^robots\.txt$', 'index.php?ezy_robots_txt=1', 'top'); 84 60 } 85 61 86 /**87 * Add custom query var for robots.txt rewrite88 */89 62 public function add_robots_query_var($vars) { 90 63 $vars[] = 'ezy_robots_txt'; … … 92 65 } 93 66 94 /**95 * Serve robots.txt via rewrite rule (bypasses physical file)96 */97 67 public function serve_robots_via_rewrite() { 98 68 if (!get_query_var('ezy_robots_txt')) { … … 121 91 } 122 92 123 /**124 * Remove any physical robots.txt files when EZY has content125 * Note: Only removes files that appear to be auto-generated (not manually created)126 */127 93 public function cleanup_competing_robots_files() { 128 94 if (!$this->get_robots_txt_content()) { … … 147 113 foreach ($paths_to_check as $file_path) { 148 114 if (file_exists($file_path) && wp_is_writable($file_path)) { 149 // Check if this is a Yoast-generated file or generic auto-generated150 115 $content = @file_get_contents($file_path); 151 116 if ($content !== false) { 152 // Check for Yoast signature or generic WordPress robots153 117 $is_auto_generated = ( 154 118 strpos($content, 'Yoast') !== false || 155 119 strpos($content, '# START YOAST BLOCK') !== false || 156 // Generic WordPress default robots.txt pattern157 120 (strpos($content, 'User-agent: *') !== false && 158 121 strpos($content, 'Disallow: /wp-admin/') !== false && … … 223 186 } 224 187 225 /**226 * Static method for plugin activation227 */228 188 public static function activate() { 229 189 add_rewrite_rule('^robots\.txt$', 'index.php?ezy_robots_txt=1', 'top'); … … 231 191 } 232 192 233 /**234 * Static method for plugin deactivation235 */236 193 public static function deactivate() { 237 // Nothing special needed for robots.txt238 194 } 239 195 -
ezy-ai/trunk/includes/class-ezy-ai-schema.php
r3440881 r3445423 1 1 <?php 2 /**3 * EZY AI Schema.org Markup Handler4 *5 * Handles schema.org JSON-LD injection with guaranteed priority over Yoast SEO.6 * Uses multiple strategies to ensure EZY schemas take precedence.7 */8 2 9 3 if (!defined('ABSPATH')) { … … 20 14 private $auth; 21 15 22 /**23 * Cache for current page schemas to avoid repeated lookups24 */25 16 private $current_page_schemas_cache = null; 26 17 27 /**28 * Flag to track if we've already determined schemas for this request29 */30 18 private $schemas_checked = false; 31 19 … … 38 26 } 39 27 40 /**41 * Comprehensive Yoast schema blocking42 * Uses multiple filters and strategies to ensure EZY schemas take priority43 */44 28 private function disable_yoast_schema_interference() { 45 29 add_filter('wpseo_json_ld_output', array($this, 'block_yoast_jsonld_output'), 1, 1); … … 58 42 } 59 43 60 /**61 * Primary blocking method: Return FALSE to completely disable Yoast's JSON-LD output62 * This is the most effective method based on Yoast's schema-presenter.php63 */64 44 public function block_yoast_jsonld_output($data) { 65 45 if ($this->has_ezy_schemas_for_current_page()) { … … 70 50 } 71 51 72 /**73 * Secondary blocking method: Clear the schema graph74 */75 52 public function block_yoast_schema_graph($graph, $context) { 76 53 if ($this->has_ezy_schemas_for_current_page()) { … … 81 58 } 82 59 83 /**84 * Block individual schema pieces when EZY has schemas for the page85 */86 60 public function block_yoast_schema_piece($is_needed) { 87 61 if ($this->has_ezy_schemas_for_current_page()) { … … 92 66 } 93 67 94 /**95 * Block the entire graph pieces array96 */97 68 public function block_yoast_graph_pieces($pieces, $context) { 98 69 if ($this->has_ezy_schemas_for_current_page()) { … … 103 74 } 104 75 105 /**106 * Check if EZY has schemas for the current page107 * Uses caching to avoid repeated database lookups108 */109 76 private function has_ezy_schemas_for_current_page() { 110 77 if ($this->schemas_checked) { … … 126 93 } 127 94 128 /**129 * Inject EZY schema markup into wp_head130 * Runs at priority 1 to appear before most other scripts131 */132 95 public function inject_schema_markup() { 133 96 if (!$this->schemas_checked) { … … 239 202 } 240 203 241 // Use wp_print_inline_script_tag for safe JSON-LD output (WordPress 5.7+)242 // This function handles escaping internally and is the recommended approach243 204 echo "\n<!-- EZY.AI Schema.org Markup -->\n"; 244 205 wp_print_inline_script_tag( -
ezy-ai/trunk/includes/class-ezy-ai-tracking.php
r3440881 r3445423 17 17 $this->auth = $ezy_ai_auth; 18 18 19 // Hook into WordPress request lifecycle20 // Use template_redirect for frontend requests only21 19 add_action('template_redirect', array($this, 'track_visit'), 1); 22 20 } … … 24 22 public function track_visit() 25 23 { 26 // Only track if connected27 24 if (!$this->auth || !$this->auth->is_connected()) { 28 25 return; 29 26 } 30 27 31 // Skip tracking for admin, AJAX, cron, and REST API requests32 28 if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST)) { 33 29 return; 34 30 } 35 31 36 // Skip tracking for non-GET requests (except HEAD)37 32 $method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET'; 38 33 if ($method !== 'GET' && $method !== 'HEAD') { … … 40 35 } 41 36 42 // Get connection token43 37 $token = $this->auth->get_connection_token(); 44 38 if (!$token) { … … 46 40 } 47 41 48 // Extract visit data49 42 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; 50 43 $ip_address = $this->get_client_ip(); 51 44 $url = $this->get_current_url(); 52 45 53 // Extract all headers for signature verification54 46 $headers = $this->get_all_headers(); 55 47 56 // Send tracking request asynchronously (non-blocking)57 48 $this->send_tracking_request($token, $user_agent, $ip_address, $url, $headers); 58 49 } … … 72 63 $ip = sanitize_text_field(wp_unslash($_SERVER[$key])); 73 64 74 // Handle comma-separated IPs (X-Forwarded-For)75 65 if (strpos($ip, ',') !== false) { 76 66 $ips = explode(',', $ip); … … 78 68 } 79 69 80 // Validate IP address81 70 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { 82 71 return $ip; … … 121 110 ); 122 111 123 // Get all HTTP headers124 112 if (function_exists('getallheaders')) { 125 113 $all_headers = getallheaders(); … … 132 120 } 133 121 } else { 134 // Fallback for servers without getallheaders()135 122 foreach ($_SERVER as $key => $value) { 136 123 if (strpos($key, 'HTTP_') === 0) { … … 143 130 } 144 131 145 // Ensure signature headers are included (case-insensitive)146 132 $signature_headers = array('signature-agent', 'signature-input', 'signature'); 147 133 foreach ($signature_headers as $sig_header) { … … 157 143 private function send_tracking_request($token, $user_agent, $ip_address, $url, $headers) 158 144 { 159 // Use wp_remote_post for async request (non-blocking)160 145 $api_url = EZY_AI_API_Client::API_BASE_URL . '/track'; 161 146 … … 174 159 )), 175 160 'timeout' => 5, 176 'blocking' => false, // Non-blocking request161 'blocking' => false, 177 162 'sslverify' => true, 178 163 ); 179 164 180 // Fire and forget - don't wait for response181 165 wp_remote_post($api_url, $request_args); 182 166 } -
ezy-ai/trunk/includes/class-ezy-ai-webhook.php
r3444216 r3445423 123 123 $updated_at = $request->get_param('updated_at'); 124 124 125 // swallow debug log126 127 125 $result = false; 128 126 … … 308 306 } 309 307 310 // If content is explicitly empty string, clear FAQs311 308 if ($content === '') { 312 309 return $ezy_ai_faqs->clear_faqs_html(); 313 310 } 314 311 315 // If content is null or false, return false316 312 if (empty($content)) { 317 313 return false; -
ezy-ai/trunk/includes/class-ezy-ai-widget-fetch.php
r3440881 r3445423 124 124 125 125 private function fetch_robots() { 126 // Always fetch live robots.txt from the website127 126 $site_url = get_site_url(); 128 127 $robots_url = rtrim($site_url, '/') . '/robots.txt'; … … 155 154 156 155 private function fetch_llms() { 157 // Always fetch live llms.txt from the website158 156 $site_url = get_site_url(); 159 157 $llms_url = rtrim($site_url, '/') . '/llms.txt'; … … 204 202 205 203 private function fetch_schema($page_urls = null) { 206 // Always fetch live schemas from pages207 204 $live_schemas = array(); 208 205 $total_pages_requested = 0; … … 239 236 240 237 private function fetch_meta($page_urls = null) { 241 // Return ALL cached meta descriptions - let the backend do the filtering242 // This is more reliable than trying to match URLs in WordPress243 238 global $ezy_ai_meta_descriptions; 244 239 if (!$ezy_ai_meta_descriptions) { … … 246 241 } 247 242 248 // Get all cached meta descriptions249 243 $all_cached = get_option(EZY_AI_Meta_Descriptions::META_DESCRIPTIONS_OPTION); 250 244 … … 277 271 278 272 private function fetch_sitemap() { 279 // Always fetch live sitemap.xml from the website280 273 $site_url = get_site_url(); 281 274 $sitemap_url = rtrim($site_url, '/') . '/sitemap.xml'; … … 316 309 $updated_at = get_option('ezy_ai_facts_json_updated_at'); 317 310 318 // If no stored content, try to fetch live facts.json from the website319 311 if (empty($content)) { 320 312 $site_url = get_site_url(); … … 400 392 $schemas = array(); 401 393 402 // Extract JSON-LD schemas from <script type="application/ld+json">403 394 preg_match_all('/<script[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $html, $matches); 404 395 … … 431 422 } 432 423 433 // Extract meta description from <meta name="description">434 424 $meta_description = ''; 435 425 $is_ezy_generated = false; 436 426 437 // Try to find meta description tag438 427 if (preg_match('/<meta[^>]*name=["\']description["\'][^>]*>/i', $html, $matches, PREG_OFFSET_CAPTURE)) { 439 428 $full_match = $matches[0][0]; 440 429 $match_index = $matches[0][1]; 441 430 442 // Extract content value443 431 if (preg_match('/content=["\']([^"\']*)["\']/i', $full_match, $content_matches)) { 444 432 $meta_description = html_entity_decode($content_matches[1], ENT_QUOTES, 'UTF-8'); 445 433 446 // Check for data-ezy-generated attribute447 434 if (preg_match('/data-ezy-generated\s*=\s*["\']true["\']/i', $full_match)) { 448 435 $is_ezy_generated = true; 449 436 } else { 450 // Check for EZY.AI comment within 200 chars before or after451 437 $before_context = substr($html, max(0, $match_index - 200), min(200, $match_index)); 452 438 $after_context = substr($html, $match_index + strlen($full_match), 200);
Note: See TracChangeset
for help on using the changeset viewer.