PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.5.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.5.23
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync.php

class-metasync.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.5.23, at includes/class-metasync.php

1,114 lines 38.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7
8 /**
9 * The file that defines the core plugin class
10 *
11 * A class definition that includes attributes and functions used across both the
12 * public-facing side of the site and the admin area.
13 *
14 * @link https://searchatlas.com
15 * @since 1.0.0
16 *
17 * @package Metasync
18 * @subpackage Metasync/includes
19 */
20
21 /**
22 * The core plugin class.
23 *
24 * This is used to define internationalization, admin-specific hooks, and
25 * public-facing site hooks.
26 *
27 * Also maintains the unique identifier of this plugin as well as the current
28 * version of the plugin.
29 *
30 * @since 1.0.0
31 * @package Metasync
32 * @subpackage Metasync/includes
33 * @author Engineering Team <support@searchatlas.com>
34 */
35 class Metasync
36 {
37
38 /**
39 * The loader that's responsible for maintaining and registering all hooks that power
40 * the plugin.
41 *
42 * @since 1.0.0
43 * @access protected
44 * @var Metasync_Loader $loader Maintains and registers all hooks for the plugin.
45 */
46 protected $loader;
47
48 /**
49 * The unique identifier of this plugin.
50 *
51 * @since 1.0.0
52 * @access protected
53 * @var string $plugin_name The string used to uniquely identify this plugin.
54 */
55 protected $plugin_name;
56
57 /**
58 * The current version of the plugin.
59 *
60 * @since 1.0.0
61 * @access protected
62 * @var string $version The current version of the plugin.
63 */
64 protected $version;
65
66 protected $database;
67
68 protected $db_redirection;
69
70 protected $db_heartbeat_errors;
71
72
73
74 public const option_name = "metasync_options";
75
76 /**
77 * Search Atlas Domain Constants
78 * Centralized constants for all Search Atlas service endpoints
79 */
80 public const HOMEPAGE_DOMAIN = "https://searchatlas.com";
81 public const DASHBOARD_DOMAIN = "https://dashboard.searchatlas.com";
82 public const API_DOMAIN = "https://api.searchatlas.com";
83 public const CA_API_DOMAIN = "https://ca.searchatlas.com";
84 public const SUPPORT_EMAIL = "support@searchatlas.com";
85 public const DOCUMENTATION_DOMAIN = "https://help.searchatlas.com";
86
87 /**
88 * Define the core functionality of the plugin.
89 *
90 * Set the plugin name and the plugin version that can be used throughout the plugin.
91 * Load the dependencies, define the locale, and set the hooks for the admin area and
92 * the public-facing side of the site.
93 *
94 * @since 1.0.0
95 */
96 public function __construct()
97 {
98 if (defined('METASYNC_VERSION')) {
99 $this->version = METASYNC_VERSION;
100 } else {
101 $this->version = '1.0.0';
102 }
103 $this->plugin_name = 'metasync';
104
105 $this->load_dependencies();
106 // $this->set_locale(); // Language support removed - using default only
107 $this->init_api_key_monitor();
108 $this->define_admin_hooks();
109 $this->define_public_hooks();
110 }
111
112 /**
113 * Load the required dependencies for this plugin.
114 *
115 * Include the following files that make up the plugin:
116 *
117 * - Metasync_Loader. Orchestrates the hooks of the plugin.
118 * - Metasync_i18n. Defines internationalization functionality.
119 * - Metasync_Admin. Defines all hooks for the admin area.
120 * - Metasync_Public. Defines all hooks for the public side of the site.
121 *
122 * Create an instance of the loader which will be used to register the hooks
123 * with WordPress.
124 *
125 * @since 1.0.0
126 * @access private
127 */
128 private function load_dependencies()
129 {
130
131 /**
132 * The class responsible for orchestrating the actions and filters of the
133 * core plugin.
134 */
135 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-loader.php';
136
137 /**
138 * The class responsible for defining internationalization functionality
139 * of the plugin.
140 */
141 // require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-i18n.php'; // Language support removed
142
143 /**
144 * The class responsible for heartbeat / connection-monitoring logic
145 */
146 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-heartbeat-manager.php';
147
148 /**
149 * The class responsible for monitoring API key changes and triggering heartbeat updates
150 */
151 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-api-key-monitor.php';
152
153 /**
154 * The class responsible for OTTO cache management logic
155 */
156 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-otto-cache-manager.php';
157
158 /**
159 * The class responsible for debug / error-logging logic
160 */
161 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-debug-manager.php';
162
163 /**
164 * SearchAtlas Connect / SSO Authentication Manager
165 */
166 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-connect-manager.php';
167
168 /**
169 * Compatibility Checker (extracted from admin)
170 */
171 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-compatibility-checker.php';
172
173 /**
174 * Settings field callbacks and accordion/section helpers (extracted from admin).
175 */
176 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-settings-fields.php';
177 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-settings-registration.php';
178 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-admin-navigation.php';
179 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-admin-ajax.php';
180 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-admin-assets.php';
181
182 /**
183 * The class responsible for defining all actions that occur in the admin area.
184 */
185 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-admin.php';
186 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-admin-pages.php';
187 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-post-meta-setting.php';
188 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-edge-cache-settings.php';
189
190 /**
191 * The class responsible for the setup wizard functionality.
192 */
193 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-setup-wizard.php';
194
195 /**
196 * The class responsible for displaying and managing error logs in admin settings.
197 */
198 require_once plugin_dir_path(dirname(__FILE__)) . 'site-error-logs/class-metasync-error-logs.php';
199
200 /**
201 * The class responsible for defining all actions that occur in the public-facing
202 * side of the site.
203 */
204 require_once plugin_dir_path(dirname(__FILE__)) . 'public/class-metasync-public.php';
205 require_once plugin_dir_path(dirname(__FILE__)) . 'public/class-metasync-rest-api.php';
206 require_once plugin_dir_path(dirname(__FILE__)) . 'public/class-metasync-seo-output.php';
207
208 /**
209 * The class responsible for defining template crawling and check feture image and post_title
210 * side of the site.
211 */
212 require plugin_dir_path(dirname(__FILE__)) . 'public/class-metasync-hidden-post.php';
213
214
215
216
217
218 /**
219 * The class responsible for defining all actions that occur in the public-facing
220 * side of the site.
221 */
222 require_once plugin_dir_path(dirname(__FILE__)) . 'code-snippets/class-metasync-code-snippets.php';
223
224 /**
225 * The class responsible for Open Graph and Twitter Card meta tags functionality
226 */
227 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-opengraph.php';
228
229
230
231 /**
232 * The class responsible for defining all actions that occur in the public-facing
233 * side of the site.
234 */
235 require_once plugin_dir_path(dirname(__FILE__)) . 'optimal-settings/class-metasync-optimal-settings.php';
236
237
238
239
240 require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
241
242 /**
243 * The class responsible for defining all actions that occur in the public-facing
244 * side of the site.
245 */
246 require_once plugin_dir_path(dirname(__FILE__)) . 'customer-sync-requests/class-metasync-sync-requests.php';
247
248 /**
249 * The class responsible for orchestrating the actions and filters of the
250 * core plugin.
251 */
252 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-common.php';
253
254 /**
255 * The class responsible for access control functionality
256 */
257 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-access-control.php';
258 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-access-control-ui.php';
259
260 /**
261 * The class responsible for authentication management
262 */
263 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-auth-manager.php';
264
265 /**
266 * The class responsible for endpoint management (production/staging switching)
267 */
268 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-endpoint-manager.php';
269
270 /**
271 * The class responsible for developer panel functionality
272 */
273 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-dev-panel.php';
274
275 /**
276 * The class responsible for defining all actions that occur in the admin area.
277 */
278 require_once plugin_dir_path(dirname(__FILE__)) . 'heartbeat-error-monitor/class-metasync-heartbeat-error-monitor-database.php';
279 require_once plugin_dir_path(dirname(__FILE__)) . 'heartbeat-error-monitor/class-metasync-heartbeat-error-monitor.php';
280
281 /**
282 * The class responsible for redirection functionality.
283 */
284 require_once plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-redirection-database.php';
285 require_once plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-redirection.php';
286 require_once plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-auto-redirect.php';
287 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-redirections-admin.php';
288
289 /**
290 * The class responsible for custom HTML pages functionality.
291 */
292 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-custom-pages.php';
293
294 /**
295 * The class responsible for defining all actions that occur in the template.
296 */
297 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-template.php';
298
299 /**
300 * The class responsible for Google Index API functionality
301 */
302 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
303
304 /**
305 * The class responsible for Schema Markup functionality.
306 */
307 require_once plugin_dir_path(dirname(__FILE__)) . 'schema-markup/class-metasync-schema-markup.php';
308
309 /**
310 * The class responsible for OTTO Frontend Toolbar functionality.
311 */
312 require_once plugin_dir_path(dirname(__FILE__)) . 'otto-frontend-toolbar/class-metasync-otto-frontend-toolbar.php';
313
314 /**
315 * The class responsible for SEO Sidebar in Gutenberg Block Editor.
316 */
317 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-seo-sidebar.php';
318
319 /**
320 * The class responsible for enhanced error logging with categories.
321 */
322 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-error-logger.php';
323
324 /**
325 * The class responsible for displaying the review/rate plugin notice.
326 */
327 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-review-notice.php';
328
329 /**
330 * The class responsible for Site Health integration
331 */
332 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-site-health.php';
333
334 $this->loader = new Metasync_Loader();
335 $this->db_heartbeat_errors = new Metasync_HeartBeat_Error_Monitor_Database();
336 $this->db_redirection = new Metasync_Redirection_Database();
337 }
338
339 /**
340 * Define the locale for this plugin for internationalization.
341 *
342 * Uses the Metasync_i18n class in order to set the domain and to register the hook
343 * with WordPress.
344 *
345 * @since 1.0.0
346 * @access private
347 */
348 // Language support removed - using default only
349 /*
350 private function set_locale()
351 {
352 $plugin_i18n = new Metasync_i18n();
353
354 $this->loader->add_action('plugins_loaded', $plugin_i18n, 'load_plugin_textdomain');
355 }
356 */
357
358 /**
359 * Initialize the API Key Monitor for comprehensive API key change detection
360 *
361 * @since 1.0.0
362 * @access private
363 */
364 private function init_api_key_monitor()
365 {
366 // Initialize the singleton instance of the API Key Monitor
367 // This will automatically set up hooks to monitor all API key changes
368 Metasync_API_Key_Monitor::get_instance();
369
370 // Log successful initialization
371 #commented out to stop appending this to error.php
372 # error_log('MetaSync: API Key Monitor initialized successfully');
373 }
374
375 /**
376 * Register all of the hooks related to the admin area functionality
377 * of the plugin.
378 *
379 * @since 1.0.0
380 * @access private
381 */
382 private function define_admin_hooks()
383 {
384
385 $plugin_admin = new Metasync_Admin($this->get_plugin_name(), $this->get_version(), $this->database, $this->db_redirection, $this->db_heartbeat_errors); // , $this->data_error_log_list
386
387 // Initialize HTML Visual Editor
388 require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-html-visual-editor.php';
389 $html_visual_editor = new Metasync_HTML_Visual_Editor($this->get_plugin_name(), $this->get_version());
390 $html_visual_editor->init();
391
392 // Initialize OTTO Debug class for developers
393 if (class_exists('Metasync_Otto_Debug')) {
394 $otto_debug = new Metasync_Otto_Debug($this->get_plugin_name(), $this->get_version());
395 }
396
397 // Initialize SEO Sidebar for Gutenberg Block Editor
398 if (class_exists('Metasync_SEO_Sidebar')) {
399 new Metasync_SEO_Sidebar($this->get_version());
400 }
401
402 $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles');
403 $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts');
404
405 # Redirection import AJAX handler
406 $redirection_handler = new Metasync_Redirection($this->db_redirection);
407 $this->loader->add_action('wp_ajax_metasync_import_redirections', $redirection_handler, 'handle_import_ajax');
408
409 // HeartBeat API Receive Respond and Settings.
410 $this->loader->add_action('heartbeat_settings', $plugin_admin, 'metasync_heartbeat_settings');
411 $this->loader->add_action('heartbeat_received', $plugin_admin, 'metasync_received_data', 10, 2);
412 $this->loader->add_action('wp_ajax_lgSendCustomerParams', $plugin_admin, 'lgSendCustomerParams');
413
414 // Search Atlas Connect endpoints - authenticates with Search Atlas platform to retrieve SA API key and Otto UUID
415 $this->loader->add_action('wp_ajax_generate_searchatlas_connect_url', $plugin_admin, 'generate_searchatlas_connect_url');
416 $this->loader->add_action('wp_ajax_check_searchatlas_connect_status', $plugin_admin, 'check_searchatlas_connect_status');
417 $this->loader->add_action('wp_ajax_reset_searchatlas_authentication', $plugin_admin, 'reset_searchatlas_authentication');
418
419 // Auto-update filter
420 $this->loader->add_filter('auto_update_plugin', $plugin_admin, 'control_plugin_auto_updates', 10, 2);
421
422 // Search Atlas Connect development/testing endpoints
423 $this->loader->add_action('wp_ajax_test_enhanced_searchatlas_tokens', $plugin_admin, 'test_enhanced_searchatlas_tokens');
424 $this->loader->add_action('wp_ajax_test_whitelabel_domain', $plugin_admin, 'test_whitelabel_domain');
425 $this->loader->add_action('wp_ajax_test_searchatlas_ajax_endpoint', $plugin_admin, 'test_searchatlas_ajax_endpoint');
426 $this->loader->add_action('wp_ajax_simple_ajax_test', $plugin_admin, 'simple_ajax_test');
427
428
429 $post_meta_setting = new Metasync_Post_Meta_Settings();
430 $this->loader->add_action('admin_init', $post_meta_setting, 'add_post_meta_data', 2);
431 $this->loader->add_action('admin_init', $post_meta_setting, 'show_top_admin_bar', 9);
432 $this->loader->add_action('wp', $post_meta_setting, 'show_top_admin_bar', 9);
433
434 // Initialize XML Sitemap auto-update hooks if enabled (admin only)
435 if (is_admin() && get_option('metasync_sitemap_auto_update', false)) {
436 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
437 $sitemap_generator = new Metasync_Sitemap_Generator();
438 $sitemap_generator->setup_auto_update_hooks();
439 }
440 // Initialize Schema Markup functionality
441 $schema_markup = new Metasync_Schema_Markup($this->get_plugin_name(), $this->get_version());
442 $this->loader->add_action('wp_ajax_metasync_get_schema_fields', $schema_markup, 'ajax_get_schema_fields');
443 $this->loader->add_action('wp_ajax_metasync_preview_schema', $schema_markup, 'ajax_preview_schema');
444
445 // Initialize Developer Panel (for endpoint switching)
446 if (class_exists('Metasync_Dev_Panel')) {
447 $dev_panel = new Metasync_Dev_Panel($this->get_plugin_name(), $this->get_version());
448 }
449
450 // Initialize Site Health integration
451 if (class_exists('Metasync_Site_Health')) {
452 $site_health = new Metasync_Site_Health();
453 $site_health->register_tests();
454 }
455
456 // Initialize endpoint URL filtering for staging mode
457 $this->init_endpoint_filtering();
458
459 }
460
461 /**
462 * Register all of the hooks related to the public-facing functionality
463 * of the plugin.
464 *
465 * @since 1.0.0
466 * @access private
467 */
468 private function define_public_hooks()
469 {
470 // Header and Footer code snippets
471 $code_snippets = new Metasync_Code_Snippets();
472
473 $this->loader->add_action('wp_head', $code_snippets, 'get_header_snippet');
474 $this->loader->add_action('wp_footer', $code_snippets, 'get_footer_snippet');
475
476
477
478 $optimal_settings = new Metasync_Optimal_Settings();
479 $this->loader->add_filter('wp_robots', $optimal_settings, 'add_robots_meta');
480 $this->loader->add_action('the_content', $optimal_settings, 'add_attributes_external_links');
481
482 $plugin_public = new Metasync_Public($this->get_plugin_name(), $this->get_version());
483 $rest_api = $plugin_public->get_rest_api();
484 $seo_output = $plugin_public->get_seo_output();
485 $get_plugin_basename = sprintf('%1$s/%1$s.php', $this->plugin_name);
486
487 // Asset enqueue hooks (Metasync_Public)
488 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles');
489 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts');
490 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_page_custom_css', 999);
491
492 // Elementor editor CSS injection
493 if (class_exists('\Elementor\Plugin')) {
494 $this->loader->add_action('elementor/preview/enqueue_styles', $plugin_public, 'enqueue_elementor_editor_css', 999);
495 }
496
497 // Divi builder CSS injection
498 if (function_exists('et_setup_theme')) {
499 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_divi_builder_css', 999);
500 }
501
502 // SEO Output hooks (Metasync_Seo_Output)
503 $this->loader->add_action('wp_head', $seo_output, 'hook_metasync_metatags', 1, 1);
504 $this->loader->add_action('template_redirect', $seo_output, 'inject_archive_seo_controls');
505
506 // Edge Cache: detect Cloudways Varnish and persist for settings UI
507 $this->loader->add_action('init', 'Metasync_Edge_Cache_Purge', 'detect_cloudways');
508
509 // Sitemap exclusions for disabled archive types
510 $this->loader->add_filter('wp_sitemaps_taxonomies', $seo_output, 'filter_sitemap_taxonomies');
511 $this->loader->add_filter('wp_sitemaps_users_entry', $seo_output, 'filter_sitemap_users', 10, 2);
512 $this->loader->add_filter('wp_sitemaps_add_provider', $seo_output, 'filter_sitemap_providers', 10, 2);
513 $this->loader->add_filter('wp_sitemaps_index_entry', $seo_output, 'filter_sitemap_index_entries', 10, 4);
514
515 // AMP cleanup functionality - remove metasync_optimized attribute from head on AMP pages
516 $this->loader->add_action('template_redirect', $seo_output, 'cleanup_amp_head_attribute', 1);
517 $this->loader->add_action('wp_footer', $seo_output, 'end_amp_head_cleanup', 999);
518
519 // Redirection functionality
520 $redirection = new Metasync_Redirection($this->db_redirection);
521 $this->loader->add_action('template_redirect', $redirection, 'handle_template_redirect', 5);
522
523 # Prevent WordPress from redirecting to draft posts via redirect_canonical
524 $this->loader->add_filter('redirect_canonical', $redirection, 'prevent_draft_post_redirects', 10, 2);
525
526 # Prevent WordPress old slug redirects to unpublished posts only
527 $this->loader->add_filter('old_slug_redirect_post_id', $redirection, 'prevent_old_slug_redirect_to_drafts', 10, 1);
528
529 // Auto-redirect on slug change - creates 301 redirect when post/page slug is changed
530 $auto_redirect = new Metasync_Auto_Redirect($this->db_redirection);
531 $auto_redirect->init();
532
533 # Custom HTML Pages functionality
534 # No additional loader hooks needed - class registers its own hooks
535 $custom_pages = new Metasync_Custom_Pages();
536
537 // 404 Error monitoring
538 $this->loader->add_action('template_redirect', $this, 'handle_404_monitoring', 10);
539 $this->loader->add_action('plugin_action_links_' . $get_plugin_basename, $plugin_public, 'metasync_plugin_links');
540
541 // REST API hooks (Metasync_Rest_Api)
542 $this->loader->add_action('rest_api_init', $rest_api, 'metasync_register_rest_routes');
543 $this->loader->add_action('init', $plugin_public, 'metasync_plugin_init', 5);
544 $this->loader->add_action('wp_ajax_metasync', $rest_api, 'sync_items');
545 $this->loader->add_action('wp_ajax_lglogin', $rest_api, 'linkgraph_login');
546
547 // Robots meta filter (Metasync_Seo_Output)
548 $this->loader->add_filter('wp_robots', $seo_output, 'metasync_wp_robots_meta');
549
550
551
552 $metasyncTemplateClass = new Metasync_Template();
553 $this->loader->add_filter('theme_page_templates', $metasyncTemplateClass, 'metasync_template_landing_page', 10, 3);
554 $this->loader->add_filter('template_include', $metasyncTemplateClass, 'metasync_template_landing_page_load', 99 );
555 $templateCrawler = new MetaSyncHiddenPostManager(); # initialize the crawler class
556
557 $this->loader->add_action('wp_trash_post', $templateCrawler , 'prevent_post_deletion'); # Prevent post deletion when moved to trash
558 $this->loader->add_action('before_delete_post', $templateCrawler , 'prevent_post_deletion'); # Prevent permanent deletion
559 # $this->loader->add_filter('metasync_hidden_post_manager', $templateCrawler , 'init'); # run the crawler
560 # Hidden post manager now runs via cron instead of filter (to avoid interfering with post create/update)
561 $this->loader->add_action('metasync_hidden_post_check', $templateCrawler , 'init'); # run the crawler via cron
562
563 // Open Graph and Social Media Tags
564 $opengraph = new Metasync_OpenGraph($this->get_plugin_name(), $this->get_version());
565 $opengraph->init();
566
567 # Save current theme info to database (safe context - admin/init hooks)
568 $this->loader->add_action('after_switch_theme', $this, 'save_current_theme_info');
569 $this->loader->add_action('admin_init', $this, 'ensure_theme_info_saved');
570
571 // OTTO Frontend Toolbar
572 $otto_toolbar = new Metasync_Otto_Frontend_Toolbar($this->get_plugin_name(), $this->get_version());
573 $this->loader->add_action('wp_enqueue_scripts', $otto_toolbar, 'enqueue_styles');
574 $this->loader->add_action('wp_enqueue_scripts', $otto_toolbar, 'enqueue_scripts');
575 $this->loader->add_action('admin_bar_menu', $otto_toolbar, 'add_admin_bar_menu', 100);
576 $this->loader->add_action('wp_footer', $otto_toolbar, 'render_debug_bar', 999);
577
578 // Initialize Sitemap Generator on frontend (for virtual sitemap serving)
579 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
580 $sitemap_generator = new Metasync_Sitemap_Generator();
581 }
582
583 /**
584 * Initialize endpoint URL filtering for staging mode
585 * Intercepts HTTP requests and replaces production URLs with staging URLs
586 */
587 private function init_endpoint_filtering() {
588 // Only add filter if Endpoint Manager is available and staging mode is active
589 if (!class_exists('Metasync_Endpoint_Manager') || !Metasync_Endpoint_Manager::is_staging_mode()) {
590 return;
591 }
592
593 // Add filter to intercept HTTP requests before they're sent
594 add_filter('pre_http_request', array($this, 'filter_http_request_urls'), 10, 3);
595 }
596
597 /**
598 * Filter HTTP request URLs to replace production endpoints with staging
599 *
600 * @param false|array|WP_Error $preempt Whether to preempt an HTTP request's return value.
601 * @param array $args HTTP request arguments.
602 * @param string $url The request URL.
603 * @return false|array|WP_Error
604 */
605 public function filter_http_request_urls($preempt, $args, $url) {
606 // Only process if we're not preempting the request
607 if ($preempt !== false) {
608 return $preempt;
609 }
610
611 // Only process if staging mode is active
612 if (!class_exists('Metasync_Endpoint_Manager') || !Metasync_Endpoint_Manager::is_staging_mode()) {
613 return $preempt;
614 }
615
616 // Define URL replacements (production => staging)
617 $url_replacements = array(
618 'https://dashboard.searchatlas.com' => 'https://dashboard.staging.searchatlas.com',
619 'https://api.searchatlas.com' => 'https://api.staging.searchatlas.com',
620 'https://ca.searchatlas.com' => 'https://ca.staging.searchatlas.com',
621 'https://sa.searchatlas.com' => 'https://sa.staging.searchatlas.com',
622 );
623
624 // Check if URL needs to be replaced
625 $original_url = $url;
626 foreach ($url_replacements as $production => $staging) {
627 if (strpos($url, $production) === 0) {
628 $url = str_replace($production, $staging, $url);
629 error_log("MetaSync Endpoint Filter: Replaced {$production} with {$staging} in URL: {$original_url}");
630 break;
631 }
632 }
633
634 // If URL was changed, modify the args and make the request ourselves
635 if ($url !== $original_url) {
636 // Make the request with the modified URL
637 return wp_remote_request($url, $args);
638 }
639
640 return $preempt;
641 }
642
643 /**
644 * Save current theme information to MetaSync options
645 * This runs in WordPress admin context, not during REST API requests
646 * Safe to use wp_get_theme() here
647 */
648 public function save_current_theme_info() {
649 $theme = wp_get_theme();
650 $metasync_data = self::get_option();
651
652 if (!isset($metasync_data['general'])) {
653 $metasync_data['general'] = array();
654 }
655
656 $metasync_data['general']['current_theme_name'] = $theme->get('Name');
657 $metasync_data['general']['current_theme_template'] = $theme->get_template();
658 $metasync_data['general']['theme_info_updated'] = time();
659
660 self::set_option($metasync_data);
661 }
662
663 /**
664 * Ensure theme info is saved on admin_init if not already saved
665 * This ensures theme info is available even if theme wasn't switched
666 */
667 public function ensure_theme_info_saved() {
668 $metasync_data = self::get_option('general');
669
670 # Only run once per day to avoid overhead
671 if (empty($metasync_data['theme_info_updated']) ||
672 (time() - $metasync_data['theme_info_updated']) > 86400) {
673 $this->save_current_theme_info();
674 }
675 }
676
677 public static function get_option($key = null, $default = null)
678 {
679 $options = get_option(Metasync::option_name);
680 if (empty($options)) $options = [];
681 if ($key === null) return $options;
682 return $options[$key] ?? ($default !== null ? $default : null);
683 }
684
685 public static function set_option($data)
686 {
687 #return update_option(Metasync::option_name, $data);
688 $result = update_option(Metasync::option_name, $data);
689
690 // NEW: Structured error logging for database errors (only log if it's a real DB error)
691 global $wpdb;
692 if ($result === false && class_exists('Metasync_Error_Logger') && !empty($wpdb->last_error)) {
693 // Check if it's actually a database error (not just same value)
694 $saved_data = get_option(Metasync::option_name);
695 if ($saved_data !== $data) {
696 // Value is different but save failed - this is a real database error
697 Metasync_Error_Logger::log(
698 Metasync_Error_Logger::CATEGORY_DATABASE_ERROR,
699 Metasync_Error_Logger::SEVERITY_ERROR,
700 'Failed to save plugin main options to database',
701 [
702 'option_name' => Metasync::option_name,
703 'wpdb_error' => $wpdb->last_error,
704 'wpdb_last_query' => $wpdb->last_query,
705 'operation' => 'set_option',
706 'has_api_key' => !empty($data['general']['searchatlas_api_key'] ?? null),
707 'has_auth_token' => !empty($data['general']['apikey'] ?? null)
708 ]
709 );
710 }
711 }
712
713 return $result;
714 }
715
716 /**
717 * Get whitelabel settings
718 * Helper method to retrieve whitelabel configuration
719 */
720 public static function get_whitelabel_settings()
721 {
722 $whitelabel = self::get_option('whitelabel');
723 return is_array($whitelabel) ? $whitelabel : array(
724 'is_whitelabel' => false,
725 'domain' => '',
726 'logo' => '',
727 'company_name' => '',
728 'updated_at' => 0
729 );
730 }
731
732 /**
733 * Check if whitelabel mode is enabled
734 */
735 public static function is_whitelabel_enabled()
736 {
737 $whitelabel = self::get_whitelabel_settings();
738 return isset($whitelabel['is_whitelabel']) && $whitelabel['is_whitelabel'] === true;
739 }
740
741 /**
742 * Get effective dashboard domain for the plugin
743 * Returns whitelabel domain if set (regardless of is_whitelabel flag), otherwise respects staging/production mode
744 */
745 public static function get_dashboard_domain()
746 {
747 $whitelabel = self::get_whitelabel_settings();
748
749 // Priority 1: Use whitelabel domain if it's not empty (regardless of is_whitelabel flag)
750 if (!empty($whitelabel['domain'])) {
751 return $whitelabel['domain'];
752 }
753
754 // Priority 2: Use endpoint manager to respect staging/production mode
755 if (class_exists('Metasync_Endpoint_Manager')) {
756 return Metasync_Endpoint_Manager::get_endpoint('DASHBOARD_DOMAIN');
757 }
758
759 // Priority 3: Fallback to production default domain
760 return self::DASHBOARD_DOMAIN;
761 }
762
763 /**
764 * Get whitelabel logo URL
765 * Returns the whitelabel logo URL if logo is set
766 */
767 public static function get_whitelabel_logo()
768 {
769 $whitelabel = self::get_whitelabel_settings();
770
771 // Return logo if it's set and is a valid URL
772 // Users should be able to set a custom logo without requiring a custom domain
773 if (!empty($whitelabel['logo'])) {
774 return $whitelabel['logo'];
775 }
776
777 return null;
778 }
779
780 /**
781 * Get whitelabel company name
782 * Returns the whitelabel company name if whitelabel is active and company name is set
783 */
784 public static function get_whitelabel_company_name()
785 {
786 $whitelabel = self::get_whitelabel_settings();
787
788 // Return company name only if whitelabel is active and company name is set
789 if (isset($whitelabel['is_whitelabel']) && $whitelabel['is_whitelabel'] === true && !empty($whitelabel['company_name'])) {
790 return $whitelabel['company_name'];
791 }
792
793 return null;
794 }
795
796 /**
797 * Get whitelabel OTTO name
798 * Returns the custom OTTO name if set, otherwise returns 'OTTO'
799 */
800 public static function get_whitelabel_otto_name()
801 {
802 $general_settings = self::get_option('general');
803
804 // Return custom OTTO name if set, otherwise fallback to 'OTTO'
805 if (!empty($general_settings['whitelabel_otto_name'])) {
806 return $general_settings['whitelabel_otto_name'];
807 }
808
809 return 'OTTO';
810 }
811
812 /**
813 * Check if the current user has access to the plugin based on role settings
814 *
815 * @return bool True if user has access, false otherwise
816 */
817 public static function current_user_has_plugin_access()
818 {
819 $user = wp_get_current_user();
820 if (!$user || !$user->exists()) {
821 return false;
822 }
823
824 // Administrators always have access
825 if (in_array('administrator', (array) $user->roles)) {
826 return true;
827 }
828
829 // Get the plugin access roles setting
830 $general_options = self::get_option('general');
831
832 // If setting not configured, default to admin-only access
833 if (!isset($general_options['plugin_access_roles'])) {
834 return false;
835 }
836
837 $allowed_roles = $general_options['plugin_access_roles'];
838
839 // If it's a string (single role), convert to array
840 if (!is_array($allowed_roles)) {
841 $allowed_roles = array($allowed_roles);
842 }
843
844 // If "all" is selected, allow access
845 if (in_array('all', $allowed_roles)) {
846 return true;
847 }
848
849 // If array is empty, deny access (only admins allowed)
850 if (empty($allowed_roles)) {
851 return false;
852 }
853
854 // Check if user has any of the allowed roles
855 $user_roles = (array) $user->roles;
856 return !empty(array_intersect($user_roles, $allowed_roles));
857 }
858
859 /**
860 * Get active JWT token for Search Atlas API authentication
861 * Convenience method accessible from anywhere in the plugin
862 *
863 * @param bool $force_refresh Force generation of new token even if cached one exists
864 * @return string|false JWT token on success, false on failure
865 */
866 public static function get_jwt_token($force_refresh = false)
867 {
868 // Delegate to admin class method
869 return Metasync_Admin::get_active_jwt_token($force_refresh);
870 }
871
872 /**
873 * Get effective plugin name
874 * Returns plugin name respecting white label settings
875 * Priority: 1) white_label_plugin_name 2) company branding + base_name 3) base_name
876 */
877 public static function get_effective_plugin_name($base_name = 'Search Atlas')
878 {
879 $general_settings = self::get_option('general');
880
881 // Priority 1: Use white_label_plugin_name if set and not empty
882 if (!empty($general_settings['white_label_plugin_name'])) {
883 return $general_settings['white_label_plugin_name'];
884 }
885
886 $whitelabel = self::get_whitelabel_settings();
887
888 // Priority 2: If whitelabel is enabled and company name is provided, enhance the plugin name
889 if (isset($whitelabel['is_whitelabel']) && $whitelabel['is_whitelabel'] === true && !empty($whitelabel['company_name'])) {
890 return $whitelabel['company_name'] . ' ' . $base_name;
891 }
892
893 // Priority 3: Return base_name as fallback
894 return $base_name;
895 }
896
897 /**
898 * Centralized API Key Event Logging
899 * Provides structured logging for all API key related events with consistent formatting
900 *
901 * @since 1.0.0
902 * @param string $event_type Type of event (change, refresh, reset, etc.)
903 * @param string $api_key_type Type of API key (plugin_auth_token, searchatlas_api_key)
904 * @param array $details Additional details about the event
905 * @param string $level Log level (info, warning, error)
906 */
907 public static function log_api_key_event($event_type, $api_key_type, $details = array(), $level = 'info')
908 {
909 try {
910 // Build structured log entry
911 $log_data = array(
912 'timestamp' => current_time('mysql'),
913 'event_type' => $event_type,
914 'api_key_type' => $api_key_type,
915 'level' => $level
916 );
917
918 // Add details if provided
919 if (!empty($details)) {
920 $log_data['details'] = $details;
921 }
922
923 // Format log message with consistent structure
924 $log_prefix = strtoupper($level) . ' - MetaSync API Key Event';
925 $log_message = sprintf('[%s] %s: %s (%s)',
926 $log_data['timestamp'],
927 $log_prefix,
928 $event_type,
929 $api_key_type
930 );
931
932 // Add details to log message if present
933 if (!empty($details)) {
934 $formatted_details = array();
935 foreach ($details as $key => $value) {
936 $formatted_details[] = $key . ': ' . (is_string($value) ? $value : json_encode($value));
937 }
938 $log_message .= ' - ' . implode(', ', $formatted_details);
939 }
940
941
942 // Optionally store in database for admin dashboard (future enhancement)
943 // This could be extended to store in a dedicated log table
944
945 } catch (Exception $e) {
946 // Fallback logging if structured logging fails
947 error_log('MetaSync API Key Event Logging Error: ' . $e->getMessage());
948 }
949 }
950
951 /**
952 * Handle 404 error monitoring
953 */
954 public function handle_404_monitoring()
955 {
956 // Only process on frontend
957 if (is_admin()) {
958 return;
959 }
960
961 // Check if this is a 404 error
962 if (!is_404()) {
963 return;
964 }
965
966 // PROTECTION 1: Exclude static assets to reduce noise
967 $request_uri = $_SERVER['REQUEST_URI'] ?? '';
968 $static_extensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.ico', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.map','.webp'];
969 foreach ($static_extensions as $ext) {
970 if (stripos($request_uri, $ext) !== false) {
971 return; // Skip logging static asset 404s
972 }
973 }
974
975 // PROTECTION 2: Bot detection - Block known bot patterns
976 $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
977 $bot_patterns = ['bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python', 'java'];
978 foreach ($bot_patterns as $pattern) {
979 if (stripos($user_agent, $pattern) !== false) {
980 // Rate limit bot 404s more aggressively
981 $bot_rate_key = 'metasync_404_bot_rate';
982 $bot_hits = get_transient($bot_rate_key);
983 if ($bot_hits !== false && $bot_hits >= 10) {
984 // Bot has hit 10+ 404s in last minute - stop logging
985 return;
986 }
987 set_transient($bot_rate_key, $bot_hits === false ? 1 : $bot_hits + 1, 60);
988 break;
989 }
990 }
991
992 // PROTECTION 3: Global rate limiting - Prevent 404 logging storms
993 $global_rate_key = 'metasync_404_global_rate';
994 $global_hits = get_transient($global_rate_key);
995 if ($global_hits !== false && $global_hits >= 50) {
996 // More than 50 404s per minute - stop logging to protect database
997 if ($global_hits === 50) {
998 error_log('MetaSync 404 Monitor: Rate limit exceeded - 50+ 404s per minute. Pausing logging.');
999 }
1000 set_transient($global_rate_key, $global_hits + 1, 60);
1001 return;
1002 }
1003 set_transient($global_rate_key, $global_hits === false ? 1 : $global_hits + 1, 60);
1004
1005 // Get current URL
1006 $current_url = $this->get_current_url();
1007
1008 // PROTECTION 4: Per-URL caching - Prevent same URL from being logged repeatedly
1009 $url_cache_key = 'metasync_404_cached_' . md5($current_url);
1010 if (get_transient($url_cache_key)) {
1011 // This URL was already logged in last 5 minutes - skip DB write
1012 return;
1013 }
1014
1015 // PROTECTION 5: URL validation - Skip obviously malicious URLs
1016 if (strlen($current_url) > 500 || preg_match('/[<>{}\\\\|]/', $current_url)) {
1017 return; // Skip potentially malicious or malformed URLs
1018 }
1019
1020 // Initialize 404 monitor database
1021 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1022 $db_404 = new Metasync_Error_Monitor_Database();
1023
1024 // Get user agent (sanitized)
1025 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '';
1026
1027 // Log the 404 error
1028 $result = $db_404->update([
1029 'uri' => $current_url,
1030 'user_agent' => $user_agent,
1031 'date_time' => current_time('mysql'),
1032 'hits_count' => 1
1033 ]);
1034
1035 // Cache this URL for 5 minutes to prevent repeated DB writes
1036 set_transient($url_cache_key, true, 300);
1037 }
1038
1039 /**
1040 * Get current URL
1041 */
1042 private function get_current_url()
1043 {
1044 $protocol = is_ssl() ? 'https://' : 'http://';
1045
1046 // Safely get HTTP_HOST with fallback
1047 $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
1048 if (empty($host) && isset($_SERVER['SERVER_NAME'])) {
1049 $host = $_SERVER['SERVER_NAME'];
1050 }
1051 if (empty($host)) {
1052 // Fallback to WordPress site URL if available
1053 $host = parse_url(home_url(), PHP_URL_HOST);
1054 }
1055
1056 // Safely get REQUEST_URI with fallback
1057 $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
1058
1059 // Decode URL-encoded characters
1060 $uri = urldecode($uri);
1061
1062 // Ensure URI starts with /
1063 if (!str_starts_with($uri, '/')) {
1064 $uri = '/' . $uri;
1065 }
1066
1067 return $protocol . $host . $uri;
1068 }
1069
1070 /**
1071 * Run the loader to execute all of the hooks with WordPress.
1072 *
1073 * @since 1.0.0
1074 */
1075 public function run()
1076 {
1077 $this->loader->run();
1078 }
1079
1080 /**
1081 * The name of the plugin used to uniquely identify it within the context of
1082 * WordPress and to define internationalization functionality.
1083 *
1084 * @since 1.0.0
1085 * @return string The name of the plugin.
1086 */
1087 public function get_plugin_name()
1088 {
1089 return $this->plugin_name;
1090 }
1091
1092 /**
1093 * The reference to the class that orchestrates the hooks with the plugin.
1094 *
1095 * @since 1.0.0
1096 * @return Metasync_Loader Orchestrates the hooks of the plugin.
1097 */
1098 public function get_loader()
1099 {
1100 return $this->loader;
1101 }
1102
1103 /**
1104 * Retrieve the version number of the plugin.
1105 *
1106 * @since 1.0.0
1107 * @return string The version number of the plugin.
1108 */
1109 public function get_version()
1110 {
1111 return $this->version;
1112 }
1113 }
1114