| @@ -33,9 +33,8 @@ | ||
| 33 | 33 | const SECTION_METASYNC = "metasync_settings"; |
| 34 | 34 | const SECTION_SEARCHENGINE = "searchengine_settings"; |
| 35 | 35 | const SECTION_LOCALSEO = "local_seo"; |
| 36 | 36 | const SECTION_CODESNIPPETS = "code_snippets"; |
| 37 | - const SECTION_OPTIMAL_SETTINGS = "optimal_settings"; | |
| 38 | 37 | const SECTION_SITE_SETTINGS = "site_settings"; |
| 39 | 38 | const SECTION_COMMON_SETTINGS = "common_settings"; |
| 40 | 39 | const SECTION_COMMON_META_SETTINGS = "common_meta_settings"; |
| 41 | 40 | const SECTION_SOCIAL_META = "social_meta"; |
| @@ -42,9 +41,8 @@ | ||
| 42 | 41 | const SECTION_SEO_CONTROLS = "seo_controls"; |
| 43 | 42 | const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced"; |
| 44 | 43 | const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index"; |
| 45 | 44 | const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings"; |
| 46 | - const SECTION_BREADCRUMBS = "breadcrumbs_settings"; | |
| 47 | 45 | const SECTION_LLMS_TXT = "llms_txt_settings"; |
| 48 | 46 | |
| 49 | 47 | /** |
| 50 | 48 | * The ID of this plugin. |
| @@ -200,9 +198,9 @@ | ||
| 200 | 198 | // Set menu title using the effective title (includes whitelabel company name if available) |
| 201 | 199 | $this->menu_title = $this->get_effective_menu_title(); |
| 202 | 200 | $raw_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : ''; |
| 203 | 201 | $clean_slug = sanitize_title($raw_slug); |
| 204 | - # Self-heal a legacy URL-shaped slug (WP-413): persist the sanitized value so every | |
| 202 | + # Self-heal a legacy URL-shaped slug: persist the sanitized value so every | |
| 205 | 203 | # reader that builds admin links from the stored option gets a valid WP menu slug. |
| 206 | 204 | if ($raw_slug !== '' && $clean_slug !== $raw_slug) { |
| 207 | 205 | $options = Metasync::get_option(); |
| 208 | 206 | $options['general']['white_label_plugin_menu_slug'] = $clean_slug; |
| @@ -265,8 +263,9 @@ | ||
| 265 | 263 | add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0); |
| 266 | 264 | |
| 267 | 265 | add_action('admin_init', array($this, 'initialize_cookie')); |
| 268 | 266 | add_action('admin_init', array($this, 'maybe_redirect_to_wizard')); |
| 267 | + add_action('admin_init', array($this, 'redirect_legacy_instant_indexing_page')); | |
| 269 | 268 | |
| 270 | 269 | // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed) |
| 271 | 270 | add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins')); |
| 272 | 271 | add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all')); |
| @@ -284,8 +283,11 @@ | ||
| 284 | 283 | |
| 285 | 284 | // Add AJAX for saving execution settings |
| 286 | 285 | add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') ); |
| 287 | 286 | |
| 287 | + // Add AJAX for saving the Headless Mode demo controls on the Advanced tab | |
| 288 | + add_action( 'wp_ajax_metasync_save_headless_settings', array(Metasync_Settings_Registration::instance(), 'ajax_save_headless_settings') ); | |
| 289 | + | |
| 288 | 290 | // Add AJAX for saving hosting cache settings |
| 289 | 291 | add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') ); |
| 290 | 292 | |
| 291 | 293 | // Add AJAX for saving OTTO Cache TTL |
| @@ -315,8 +317,9 @@ | ||
| 315 | 317 | add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots')); |
| 316 | 318 | add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup')); |
| 317 | 319 | add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup')); |
| 318 | 320 | add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup')); |
| 321 | + add_action('wp_ajax_metasync_get_robots_backups', array($this, 'ajax_get_robots_backups')); | |
| 319 | 322 | |
| 320 | 323 | // Add AJAX handlers for host blocking test |
| 321 | 324 | add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get')); |
| 322 | 325 | add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post')); |
| @@ -370,10 +373,18 @@ | ||
| 370 | 373 | add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress')); |
| 371 | 374 | add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected')); |
| 372 | 375 | add_action('wp_ajax_metasync_bulk_unoptimize_selected', array($this, 'ajax_bulk_unoptimize_selected')); |
| 373 | 376 | add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick')); |
| 377 | + add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image')); | |
| 374 | 378 | add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron')); |
| 375 | 379 | |
| 380 | + # SEO Restore (Rollback) AJAX & Cron handlers | |
| 381 | + add_action('wp_ajax_metasync_seo_restore_start', array($this, 'ajax_seo_restore_start')); | |
| 382 | + add_action('wp_ajax_metasync_seo_restore_cancel', array($this, 'ajax_seo_restore_cancel')); | |
| 383 | + add_action('wp_ajax_metasync_seo_restore_progress', array($this, 'ajax_seo_restore_progress')); | |
| 384 | + add_action('wp_ajax_metasync_seo_restore_process_tick', array($this, 'ajax_seo_restore_process_tick')); | |
| 385 | + add_action('metasync_seo_restore_cron', array($this, 'handle_seo_restore_cron')); | |
| 386 | + | |
| 376 | 387 | # Add AJAX handlers for Google Instant Indexing |
| 377 | 388 | add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi')); |
| 378 | 389 | |
| 379 | 390 | # Add AJAX handlers for Bing Instant Indexing (IndexNow) |
| @@ -466,9 +477,9 @@ | ||
| 466 | 477 | |
| 467 | 478 | $data= Metasync::get_option('general'); |
| 468 | 479 | |
| 469 | 480 | # Get white label menu slug |
| 470 | - # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug (WP-413) | |
| 481 | + # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug | |
| 471 | 482 | $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']); |
| 472 | 483 | $menu_slug = $menu_slug === '' ? self::$page_slug : $menu_slug; |
| 473 | 484 | |
| 474 | 485 | ?> |
| @@ -491,19 +502,125 @@ | ||
| 491 | 502 | if ( ! isset( $_GET['page'] ) || strpos( $_GET['page'], self::$page_slug ) !== 0 ) { |
| 492 | 503 | return; |
| 493 | 504 | } |
| 494 | 505 | ?> |
| 495 | - <style>.metasync-dashboard-wrap { opacity: 0; transition: opacity 0.15s ease-in; }</style> | |
| 506 | + <style> | |
| 507 | + .metasync-dashboard-wrap { opacity: 0; transition: opacity 0.15s ease-in; } | |
| 508 | + /* | |
| 509 | + * WP prints admin notices at the top of #wpbody-content, then core's | |
| 510 | + * common.js relocates them next to .wp-header-end inside our wrap on | |
| 511 | + * DOM-ready. Keep them out of the flow until that move so they don't | |
| 512 | + * flash above the dashboard header and shove the page down. | |
| 513 | + * Once relocated they're no longer a direct child here, so this rule | |
| 514 | + * stops matching and they fade in with the wrap. We mirror core's | |
| 515 | + * relocation set exactly (.notice/.updated/.error, minus .inline and | |
| 516 | + * .below-h2); .update-nag is left alone since core never moves it. | |
| 517 | + */ | |
| 518 | + #wpbody-content > .notice:not(.inline):not(.below-h2), | |
| 519 | + #wpbody-content > .updated:not(.inline):not(.below-h2), | |
| 520 | + #wpbody-content > .error:not(.inline):not(.below-h2) { display: none !important; } | |
| 521 | + </style> | |
| 496 | 522 | <?php |
| 497 | 523 | } |
| 498 | 524 | |
| 499 | 525 | public function suppress_notices_on_wizard_page() { |
| 500 | - if ( isset( $_GET['page'] ) && strpos( $_GET['page'], '-setup-wizard' ) !== false ) { | |
| 526 | + if ( ! isset( $_GET['page'] ) ) { | |
| 527 | + return; | |
| 528 | + } | |
| 529 | + | |
| 530 | + $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); | |
| 531 | + | |
| 532 | + // Setup wizard: strip every notice for a fully focused, distraction-free screen. | |
| 533 | + if ( strpos( $page, '-setup-wizard' ) !== false ) { | |
| 501 | 534 | remove_all_actions( 'admin_notices' ); |
| 502 | 535 | remove_all_actions( 'all_admin_notices' ); |
| 536 | + return; | |
| 503 | 537 | } |
| 538 | + | |
| 539 | + // Other Search Atlas plugin pages: strip third-party notices but keep our own. | |
| 540 | + if ( strpos( $page, self::$page_slug ) === 0 ) { | |
| 541 | + $this->remove_third_party_admin_notices(); | |
| 542 | + } | |
| 504 | 543 | } |
| 505 | 544 | |
| 545 | + /** | |
| 546 | + * Remove admin-notice callbacks that don't belong to this plugin. | |
| 547 | + * | |
| 548 | + * Third-party plugins (e.g. Rank Math, Yoast) register their banners on the | |
| 549 | + * global admin_notices / all_admin_notices actions, which WordPress fires on | |
| 550 | + * every admin screen — so their notices leak onto our settings pages. We walk | |
| 551 | + * the registered callbacks and drop any whose code lives outside this plugin's | |
| 552 | + * directory, leaving every one of our own notices intact. | |
| 553 | + */ | |
| 554 | + private function remove_third_party_admin_notices() { | |
| 555 | + global $wp_filter; | |
| 556 | + | |
| 557 | + foreach ( array( 'admin_notices', 'all_admin_notices' ) as $hook ) { | |
| 558 | + if ( empty( $wp_filter[ $hook ] ) || ! ( $wp_filter[ $hook ] instanceof WP_Hook ) ) { | |
| 559 | + continue; | |
| 560 | + } | |
| 561 | + | |
| 562 | + // Collect first, then remove — avoids mutating the callbacks array mid-walk. | |
| 563 | + $to_remove = array(); | |
| 564 | + foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) { | |
| 565 | + foreach ( $callbacks as $callback ) { | |
| 566 | + if ( ! $this->is_own_admin_notice_callback( $callback['function'] ) ) { | |
| 567 | + $to_remove[] = array( $callback['function'], $priority ); | |
| 568 | + } | |
| 569 | + } | |
| 570 | + } | |
| 571 | + | |
| 572 | + foreach ( $to_remove as $entry ) { | |
| 573 | + remove_action( $hook, $entry[0], $entry[1] ); | |
| 574 | + } | |
| 575 | + } | |
| 576 | + } | |
| 577 | + | |
| 578 | + /** | |
| 579 | + * Determine whether an admin-notice callback is defined by this plugin. | |
| 580 | + * | |
| 581 | + * Ownership is decided by where the callback's code physically lives, not by | |
| 582 | + * class name — our notice owners use inconsistent prefixes (Metasync_Admin, | |
| 583 | + * Google_Index_Admin, ConfigControllerMetaSync), and a white-label install can | |
| 584 | + * rename the plugin folder. Resolving the declaring file and checking it sits | |
| 585 | + * inside this plugin's directory is robust against all of that. If a callback | |
| 586 | + * can't be introspected, we keep it rather than risk hiding a legitimate notice. | |
| 587 | + * | |
| 588 | + * @param string|array|Closure $function The registered callback. | |
| 589 | + * @return bool True when the callback's code lives inside this plugin. | |
| 590 | + */ | |
| 591 | + private function is_own_admin_notice_callback( $function ) { | |
| 592 | + // Plugin root: this file lives in <plugin>/admin, so its parent is the root. | |
| 593 | + $plugin_dir = wp_normalize_path( dirname( __DIR__ ) ) . '/'; | |
| 594 | + | |
| 595 | + try { | |
| 596 | + if ( is_array( $function ) && isset( $function[0], $function[1] ) ) { | |
| 597 | + $class = is_object( $function[0] ) ? get_class( $function[0] ) : $function[0]; | |
| 598 | + $ref = new ReflectionMethod( $class, $function[1] ); | |
| 599 | + } elseif ( $function instanceof Closure ) { | |
| 600 | + $ref = new ReflectionFunction( $function ); | |
| 601 | + } elseif ( is_string( $function ) && strpos( $function, '::' ) !== false ) { | |
| 602 | + list( $class, $method ) = explode( '::', $function, 2 ); | |
| 603 | + $ref = new ReflectionMethod( $class, $method ); | |
| 604 | + } elseif ( is_string( $function ) ) { | |
| 605 | + $ref = new ReflectionFunction( $function ); | |
| 606 | + } else { | |
| 607 | + return false; | |
| 608 | + } | |
| 609 | + } catch ( \Throwable $e ) { | |
| 610 | + // Unintrospectable callback — keep it; never hide a notice we can't classify. | |
| 611 | + return true; | |
| 612 | + } | |
| 613 | + | |
| 614 | + $file = $ref->getFileName(); | |
| 615 | + if ( ! $file ) { | |
| 616 | + // Internal/built-in callback (no source file) — not ours. | |
| 617 | + return false; | |
| 618 | + } | |
| 619 | + | |
| 620 | + return strpos( wp_normalize_path( $file ), $plugin_dir ) === 0; | |
| 621 | + } | |
| 622 | + | |
| 506 | 623 | #---------fixes issue : #95 ---------- |
| 507 | 624 | #This function is to redirect in case client changes slug on fresh install |
| 508 | 625 | #It is called by the add_option hook |
| 509 | 626 | |
| @@ -601,11 +718,56 @@ | ||
| 601 | 718 | exit; |
| 602 | 719 | } |
| 603 | 720 | } |
| 604 | 721 | |
| 722 | + /** | |
| 723 | + * Redirect the removed standalone Instant Indexing page to Indexation Control. | |
| 724 | + * | |
| 725 | + * The menu entry is gone, but bookmarks and external links can still target | |
| 726 | + * the old URL. Resolve the current white-label slug rather than assuming | |
| 727 | + * "searchatlas". | |
| 728 | + */ | |
| 729 | + public function redirect_legacy_instant_indexing_page() { | |
| 730 | + if (wp_doing_ajax() || wp_doing_cron() || !is_admin() || !isset($_GET['page'])) { | |
| 731 | + return; | |
| 732 | + } | |
| 733 | + | |
| 734 | + $page = sanitize_key(wp_unslash($_GET['page'])); | |
| 735 | + $legacy_page = self::$page_slug . '-instant-index'; | |
| 736 | + if ($page !== $legacy_page || !Metasync::current_user_has_plugin_access()) { | |
| 737 | + return; | |
| 738 | + } | |
| 739 | + | |
| 740 | + // Indexation Control is registered only when access control keeps it | |
| 741 | + // visible. Redirecting to it blindly would dead-end on an | |
| 742 | + // unregistered slug (wp_die "Invalid plugin page") for users whose | |
| 743 | + // white-label config hides that page — the exact configs where the | |
| 744 | + // removed standalone page was the feature's only UI. The bare plugin | |
| 745 | + // slug is the floor: its top-level callback resolves to the first | |
| 746 | + // reachable page, so it is always a safe target. | |
| 747 | + if (Metasync_Access_Control::user_can_access('hide_indexation_control')) { | |
| 748 | + $target = admin_url('admin.php?page=' . self::$page_slug . '-seo-controls'); | |
| 749 | + $query = []; | |
| 750 | + foreach (['tab', 'subtab'] as $key) { | |
| 751 | + if (isset($_GET[$key])) { | |
| 752 | + $query[$key] = sanitize_key(wp_unslash($_GET[$key])); | |
| 753 | + } | |
| 754 | + } | |
| 755 | + if (!empty($query)) { | |
| 756 | + $target = add_query_arg($query, $target); | |
| 757 | + } | |
| 758 | + } else { | |
| 759 | + $target = admin_url('admin.php?page=' . self::$page_slug); | |
| 760 | + } | |
| 761 | + | |
| 762 | + wp_safe_redirect($target); | |
| 763 | + exit; | |
| 764 | + } | |
| 765 | + | |
| 605 | 766 | public function metasync_display_error_log() { |
| 606 | 767 | Metasync_Debug_Manager::instance()->metasync_display_error_log($this); |
| 607 | 768 | } |
| 769 | + | |
| 608 | 770 | public function metasync_update_wp_config() { |
| 609 | 771 | Metasync_Debug_Manager::instance()->metasync_update_wp_config(); |
| 610 | 772 | } |
| 611 | 773 | |
| @@ -863,10 +1025,41 @@ | ||
| 863 | 1025 | array('jquery'), |
| 864 | 1026 | $this->version, |
| 865 | 1027 | true |
| 866 | 1028 | ); |
| 1029 | + | |
| 1030 | + // Keeps core's admin-menu pinning in step with pages whose height | |
| 1031 | + // changes after load. Depends on 'common' so core has bound its | |
| 1032 | + // own handlers before this runs. | |
| 1033 | + wp_enqueue_script( | |
| 1034 | + $this->plugin_name . '-admin-menu-height', | |
| 1035 | + plugin_dir_url(__FILE__) . 'js/metasync-admin-menu-height.js', | |
| 1036 | + array('jquery', 'common'), | |
| 1037 | + $this->version, | |
| 1038 | + true | |
| 1039 | + ); | |
| 867 | 1040 | } |
| 868 | 1041 | |
| 1042 | + $per_page_screens = [ | |
| 1043 | + self::$page_slug . '-redirections', | |
| 1044 | + self::$page_slug . '-404-monitor', | |
| 1045 | + self::$page_slug . '-sync-log', | |
| 1046 | + self::$page_slug . '-media-optimization', | |
| 1047 | + ]; | |
| 1048 | + | |
| 1049 | + if (in_array($current_page, $per_page_screens, true)) { | |
| 1050 | + // Results-per-page selectors use this external handler instead of | |
| 1051 | + // an inline event attribute so changing a page size always reloads | |
| 1052 | + // the current admin page. | |
| 1053 | + wp_enqueue_script( | |
| 1054 | + $this->plugin_name . '-per-page', | |
| 1055 | + plugin_dir_url(__FILE__) . 'js/metasync-per-page.js', | |
| 1056 | + array(), | |
| 1057 | + $this->version, | |
| 1058 | + true | |
| 1059 | + ); | |
| 1060 | + } | |
| 1061 | + | |
| 869 | 1062 | // Dashboard iframe height (only on dashboard page) |
| 870 | 1063 | if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) { |
| 871 | 1064 | wp_enqueue_script( |
| 872 | 1065 | $this->plugin_name . '-iframe', |
| @@ -1051,9 +1244,8 @@ | ||
| 1051 | 1244 | true |
| 1052 | 1245 | ); |
| 1053 | 1246 | wp_localize_script($this->plugin_name . '-connect', 'metasyncConnectData', array( |
| 1054 | 1247 | 'optionKey' => self::option_key, |
| 1055 | - 'storedPassword' => isset($whitelabel_data['settings_password']) ? $whitelabel_data['settings_password'] : '', | |
| 1056 | 1248 | 'adminPostUrl' => admin_url('admin-post.php'), |
| 1057 | 1249 | 'exportNonce' => wp_create_nonce('metasync_export_whitelabel'), |
| 1058 | 1250 | 'ajaxUrl' => admin_url('admin-ajax.php'), |
| 1059 | 1251 | 'logoutNonceField' => wp_nonce_field('whitelabel_logout_nonce', 'whitelabel_logout_nonce', true, false), |
| @@ -1340,14 +1532,44 @@ | ||
| 1340 | 1532 | } |
| 1341 | 1533 | |
| 1342 | 1534 | /** |
| 1343 | 1535 | * Settings of HeartBeat API for admin area. |
| 1344 | - * Set time interval of send request. | |
| 1536 | + * | |
| 1537 | + * The only thing the plugin rides the beat for is a periodic admin | |
| 1538 | + * telemetry ping (the `heartbeat-send` handler in admin/js/metasync-admin.js), | |
| 1539 | + * so it slows the beat down rather than speeding it up. Two contexts are | |
| 1540 | + * left on whatever core decided, because core leans on the beat there: | |
| 1541 | + * | |
| 1542 | + * - The front end, where the beat drives wp-auth-check — the "your session | |
| 1543 | + * has expired, log in again" prompt. | |
| 1544 | + * - The editor screens, where each tick renews the post lock. A lock only | |
| 1545 | + * lives for wp_check_post_lock_window() seconds (150 by default), so any | |
| 1546 | + * interval longer than that drops the lock between beats and lets a | |
| 1547 | + * second editor silently take over a post someone still has open. | |
| 1548 | + * | |
| 1549 | + * Core clamps this to 1-3600 seconds in wp-includes/js/heartbeat.js, so an | |
| 1550 | + * over-long interval is not corrected on our behalf. | |
| 1551 | + * | |
| 1552 | + * @param array $settings Heartbeat settings array. | |
| 1553 | + * @return array Heartbeat settings array. | |
| 1345 | 1554 | */ |
| 1346 | 1555 | function metasync_heartbeat_settings($settings) |
| 1347 | 1556 | { |
| 1348 | - global $heartbeat_frequency; | |
| 1349 | - $settings['interval'] = 300; | |
| 1557 | + if (!is_admin()) { | |
| 1558 | + return $settings; | |
| 1559 | + } | |
| 1560 | + | |
| 1561 | + # $pagenow is set in wp-includes/vars.php, long before this filter runs; | |
| 1562 | + # core's own wp_heartbeat_set_suspension() reads it exactly this way. | |
| 1563 | + global $pagenow; | |
| 1564 | + $lock_dependent_screens = array('post.php', 'post-new.php', 'customize.php'); | |
| 1565 | + if (isset($pagenow) && in_array($pagenow, $lock_dependent_screens, true)) { | |
| 1566 | + return $settings; | |
| 1567 | + } | |
| 1568 | + | |
| 1569 | + # 60s matches heartbeat.js's own default and stays well inside the | |
| 1570 | + # 150s post-lock window, so nothing core does can fall through a gap. | |
| 1571 | + $settings['interval'] = 60; | |
| 1350 | 1572 | return $settings; |
| 1351 | 1573 | } |
| 1352 | 1574 | |
| 1353 | 1575 | /** |
| @@ -1517,12 +1739,12 @@ | ||
| 1517 | 1739 | $cache_count = Metasync_Otto_Transient_Cache::get_cache_count(); |
| 1518 | 1740 | |
| 1519 | 1741 | ?> |
| 1520 | 1742 | <!-- OTTO Cache TTL Setting --> |
| 1521 | - <div style="margin-bottom: 30px;"> | |
| 1522 | - <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">OTTO Cache TTL</h4> | |
| 1743 | + <div style="margin-bottom: 30px; padding-top: 20px;"> | |
| 1744 | + <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Cache TTL</h4> | |
| 1523 | 1745 | <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);"> |
| 1524 | - Configure how long <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API suggestions are cached before a fresh API call. Stale cache expires at 2× this value. | |
| 1746 | + Configure how long <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API suggestions are cached before a fresh API call. The stale fallback expires after 12 hours. | |
| 1525 | 1747 | </p> |
| 1526 | 1748 | |
| 1527 | 1749 | <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 1528 | 1750 | <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;"> |
| @@ -1528,8 +1750,9 @@ | ||
| 1528 | 1750 | <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;"> |
| 1529 | 1751 | <label for="metasync-otto-cache-ttl" style="color: var(--dashboard-text-primary); font-weight: 500;"> |
| 1530 | 1752 | Cache TTL (minutes): |
| 1531 | 1753 | </label> |
| 1754 | + <?php Metasync::render_tooltip_icon('otto_cache_ttl', 'How many minutes OTTO reuses its last SEO suggestions before fetching fresh ones. 30 is fine for most sites.'); ?> | |
| 1532 | 1755 | <input type="number" |
| 1533 | 1756 | id="metasync-otto-cache-ttl" |
| 1534 | 1757 | value="<?php echo esc_attr($this->get_otto_cache_ttl_minutes()); ?>" |
| 1535 | 1758 | min="30" |
| @@ -1673,8 +1896,9 @@ | ||
| 1673 | 1896 | <?php checked(true, !empty($hosting_settings['wpengine_enabled'])); ?> |
| 1674 | 1897 | <?php echo !$wpe_detected ? 'disabled' : ''; ?> |
| 1675 | 1898 | style="width: 16px; height: 16px; cursor: <?php echo $wpe_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1676 | 1899 | <span style="color: var(--dashboard-text-primary); font-weight: 500;">WP Engine</span> |
| 1900 | + <?php Metasync::render_tooltip_icon('hosting_cache_wpengine', 'If hosted on WP Engine, turn on so OTTO clears the host\'s built-in cache when it updates a page.'); ?> | |
| 1677 | 1901 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges Varnish + Memcached</span> |
| 1678 | 1902 | </label> |
| 1679 | 1903 | |
| 1680 | 1904 | <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 16px; cursor: pointer;"> |
| @@ -1683,8 +1907,9 @@ | ||
| 1683 | 1907 | <?php checked(true, !empty($hosting_settings['kinsta_enabled'])); ?> |
| 1684 | 1908 | <?php echo !$kinsta_detected ? 'disabled' : ''; ?> |
| 1685 | 1909 | style="width: 16px; height: 16px; cursor: <?php echo $kinsta_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1686 | 1910 | <span style="color: var(--dashboard-text-primary); font-weight: 500;">Kinsta</span> |
| 1911 | + <?php Metasync::render_tooltip_icon('hosting_cache_kinsta', 'If hosted on Kinsta, turn on so OTTO clears Kinsta\'s server cache automatically.'); ?> | |
| 1687 | 1912 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges full-page cache (kinsta_cache_purge_full)</span> |
| 1688 | 1913 | </label> |
| 1689 | 1914 | |
| 1690 | 1915 | <div style="display: flex; align-items: center; gap: 12px;"> |
| @@ -1832,9 +2057,9 @@ | ||
| 1832 | 2057 | id="metasync-targeted-object-cache" |
| 1833 | 2058 | <?php checked('1', $targeted_cache_enabled); ?> |
| 1834 | 2059 | style="width: 16px; height: 16px; cursor: pointer; margin-top: 2px; flex-shrink: 0;" /> |
| 1835 | 2060 | <span> |
| 1836 | - <span style="color: var(--dashboard-text-primary); font-weight: 500; display: block; margin-bottom: 4px;">Targeted Object Cache Purge</span> | |
| 2061 | + <span style="color: var(--dashboard-text-primary); font-weight: 500; display: block; margin-bottom: 4px;">Targeted Object Cache Purge<?php Metasync::render_tooltip_icon('targeted_object_cache_purge', 'Leave this on. Refreshes only the pages OTTO changed instead of wiping your whole site\'s memory cache — safer/faster on large sites.'); ?></span> | |
| 1837 | 2062 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;"> |
| 1838 | 2063 | When enabled, only the updated posts are evicted from the object cache (recommended for large sites). |
| 1839 | 2064 | When disabled, a full <code>wp_cache_flush()</code> is used instead. |
| 1840 | 2065 | </span> |
| @@ -2098,9 +2323,9 @@ | ||
| 2098 | 2323 | |
| 2099 | 2324 | $ttl = isset($_POST['otto_cache_ttl']) ? absint($_POST['otto_cache_ttl']) : 30; |
| 2100 | 2325 | |
| 2101 | 2326 | if ($ttl < 30 || $ttl > 1440) { |
| 2102 | - wp_send_json_error(array('message' => 'OTTO Cache TTL must be between 30 and 1440 minutes.')); | |
| 2327 | + wp_send_json_error(array('message' => sprintf('%s Cache TTL must be between 30 and 1440 minutes.', Metasync::get_whitelabel_otto_name()))); | |
| 2103 | 2328 | return; |
| 2104 | 2329 | } |
| 2105 | 2330 | |
| 2106 | 2331 | $settings = get_option('metasync_execution_settings', array()); |
| @@ -2106,9 +2331,9 @@ | ||
| 2106 | 2331 | $settings = get_option('metasync_execution_settings', array()); |
| 2107 | 2332 | $settings['otto_cache_ttl'] = $ttl; |
| 2108 | 2333 | update_option('metasync_execution_settings', $settings); |
| 2109 | 2334 | |
| 2110 | - wp_send_json_success(array('message' => 'OTTO Cache TTL saved.')); | |
| 2335 | + wp_send_json_success(array('message' => sprintf('%s Cache TTL saved.', Metasync::get_whitelabel_otto_name()))); | |
| 2111 | 2336 | } |
| 2112 | 2337 | |
| 2113 | 2338 | /** |
| 2114 | 2339 | * admin_post handler: purge WP Engine and Kinsta hosting-level caches |
| @@ -2599,8 +2824,15 @@ | ||
| 2599 | 2824 | // Handle form submissions |
| 2600 | 2825 | if (isset($_POST['metasync_media_optimization_nonce'])) { |
| 2601 | 2826 | check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce'); |
| 2602 | 2827 | |
| 2828 | + // Settings shape the whole site's conversion pipeline — restrict | |
| 2829 | + // the save (and the reset) to site admins, not just anyone who can | |
| 2830 | + // open the page via plugin_access_roles. | |
| 2831 | + if (!current_user_can('manage_options')) { | |
| 2832 | + wp_die(__('Sorry, you are not allowed to manage media optimization settings.', 'metasync')); | |
| 2833 | + } | |
| 2834 | + | |
| 2603 | 2835 | // Handle reset to defaults |
| 2604 | 2836 | if (!empty($_POST['metasync_media_reset'])) { |
| 2605 | 2837 | $defaults = Metasync_Media_Settings::get_defaults(); |
| 2606 | 2838 | Metasync_Media_Settings::save_settings($defaults); |
| @@ -2690,8 +2922,15 @@ | ||
| 2690 | 2922 | if (!$attachment_id) { |
| 2691 | 2923 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2692 | 2924 | } |
| 2693 | 2925 | |
| 2926 | + // Ownership: under the replace strategy this permanently deletes the | |
| 2927 | + // attachment's original file, so upload_files alone is not enough — | |
| 2928 | + // the caller must be allowed to edit this specific attachment. | |
| 2929 | + if (!current_user_can('edit_post', $attachment_id)) { | |
| 2930 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 2931 | + } | |
| 2932 | + | |
| 2694 | 2933 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; |
| 2695 | 2934 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php'; |
| 2696 | 2935 | $settings = Metasync_Media_Settings::get_settings(); |
| 2697 | 2936 | |
| @@ -2743,8 +2982,20 @@ | ||
| 2743 | 2982 | if (!$attachment_id) { |
| 2744 | 2983 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2745 | 2984 | } |
| 2746 | 2985 | |
| 2986 | + // Ownership: reverting deletes the attachment's converted files, so | |
| 2987 | + // restrict it to users who may edit this specific attachment. | |
| 2988 | + if (!current_user_can('edit_post', $attachment_id)) { | |
| 2989 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 2990 | + } | |
| 2991 | + | |
| 2992 | + // Replace-strategy conversions have no original to restore — reverting | |
| 2993 | + // would delete the attachment's only file. Refuse up front, like bulk revert does. | |
| 2994 | + if (!Metasync_Image_Converter::can_revert($attachment_id)) { | |
| 2995 | + wp_send_json_error(__('This image cannot be reverted. The original file no longer exists (replace strategy).', 'metasync')); | |
| 2996 | + } | |
| 2997 | + | |
| 2747 | 2998 | $success = Metasync_Image_Converter::revert_attachment($attachment_id); |
| 2748 | 2999 | |
| 2749 | 3000 | if ($success) { |
| 2750 | 3001 | wp_send_json_success(); |
| @@ -2796,8 +3047,15 @@ | ||
| 2796 | 3047 | public function ajax_batch_progress() |
| 2797 | 3048 | { |
| 2798 | 3049 | check_ajax_referer('metasync_media_opt_nonce', 'nonce'); |
| 2799 | 3050 | |
| 3051 | + // Read-only progress/stats polling — the same capability that can | |
| 3052 | + // open the media optimization page is enough, but a nonce alone | |
| 3053 | + // never was. | |
| 3054 | + if (!current_user_can('upload_files')) { | |
| 3055 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3056 | + } | |
| 3057 | + | |
| 2800 | 3058 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; |
| 2801 | 3059 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; |
| 2802 | 3060 | |
| 2803 | 3061 | $progress = Metasync_Media_Batch_Optimizer::get_progress(); |
| @@ -2828,10 +3086,19 @@ | ||
| 2828 | 3086 | $settings = Metasync_Media_Settings::get_settings(); |
| 2829 | 3087 | |
| 2830 | 3088 | $success = 0; |
| 2831 | 3089 | $failed = 0; |
| 3090 | + $denied = 0; | |
| 2832 | 3091 | |
| 2833 | 3092 | foreach ($ids as $id) { |
| 3093 | + // Per-attachment ownership: replace mode deletes originals, so | |
| 3094 | + // upload_files alone must not allow converting images the caller | |
| 3095 | + // does not own/cannot edit. | |
| 3096 | + if (!current_user_can('edit_post', $id)) { | |
| 3097 | + $denied++; | |
| 3098 | + continue; | |
| 3099 | + } | |
| 3100 | + | |
| 2834 | 3101 | if (Metasync_Image_Converter::convert_attachment($id, $settings)) { |
| 2835 | 3102 | $success++; |
| 2836 | 3103 | } else { |
| 2837 | 3104 | $failed++; |
| @@ -2840,8 +3107,9 @@ | ||
| 2840 | 3107 | |
| 2841 | 3108 | wp_send_json_success([ |
| 2842 | 3109 | 'success' => $success, |
| 2843 | 3110 | 'failed' => $failed, |
| 3111 | + 'denied' => $denied, | |
| 2844 | 3112 | ]); |
| 2845 | 3113 | } |
| 2846 | 3114 | |
| 2847 | 3115 | /** |
| @@ -2864,11 +3132,19 @@ | ||
| 2864 | 3132 | |
| 2865 | 3133 | $success = 0; |
| 2866 | 3134 | $failed = 0; |
| 2867 | 3135 | $skipped = 0; |
| 3136 | + $denied = 0; | |
| 2868 | 3137 | $errors = []; |
| 2869 | 3138 | |
| 2870 | 3139 | foreach ($ids as $id) { |
| 3140 | + // Per-attachment ownership: reverting deletes the attachment's | |
| 3141 | + // converted files, so restrict it per image, not per role. | |
| 3142 | + if (!current_user_can('edit_post', $id)) { | |
| 3143 | + $denied++; | |
| 3144 | + continue; | |
| 3145 | + } | |
| 3146 | + | |
| 2871 | 3147 | $format = get_post_meta($id, '_metasync_converted_format', true); |
| 2872 | 3148 | |
| 2873 | 3149 | if (!$format) { |
| 2874 | 3150 | $skipped++; |
| @@ -2896,8 +3172,9 @@ | ||
| 2896 | 3172 | wp_send_json_success([ |
| 2897 | 3173 | 'success' => $success, |
| 2898 | 3174 | 'failed' => $failed, |
| 2899 | 3175 | 'skipped' => $skipped, |
| 3176 | + 'denied' => $denied, | |
| 2900 | 3177 | 'errors' => $errors, |
| 2901 | 3178 | ]); |
| 2902 | 3179 | } |
| 2903 | 3180 | |
| @@ -2921,8 +3198,49 @@ | ||
| 2921 | 3198 | wp_send_json_success($progress); |
| 2922 | 3199 | } |
| 2923 | 3200 | |
| 2924 | 3201 | /** |
| 3202 | + * AJAX: Delete an orphaned media record (attachment whose file is missing). | |
| 3203 | + * | |
| 3204 | + * Only deletes when the file is confirmed missing on disk, so valid | |
| 3205 | + * attachments can never be removed through this endpoint. | |
| 3206 | + */ | |
| 3207 | + public function ajax_delete_orphaned_image() | |
| 3208 | + { | |
| 3209 | + check_ajax_referer('metasync_media_opt_nonce', 'nonce'); | |
| 3210 | + | |
| 3211 | + $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; | |
| 3212 | + if (!$attachment_id) { | |
| 3213 | + wp_send_json_error(__('Invalid attachment ID.', 'metasync')); | |
| 3214 | + } | |
| 3215 | + | |
| 3216 | + if (!current_user_can('delete_post', $attachment_id)) { | |
| 3217 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3218 | + } | |
| 3219 | + | |
| 3220 | + if (get_post_type($attachment_id) !== 'attachment') { | |
| 3221 | + wp_send_json_error(__('Not an attachment.', 'metasync')); | |
| 3222 | + } | |
| 3223 | + | |
| 3224 | + require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; | |
| 3225 | + | |
| 3226 | + // Guard: only orphaned records (missing file) may be deleted here. | |
| 3227 | + if (!Metasync_Media_Library_List_Table::is_file_missing($attachment_id)) { | |
| 3228 | + wp_send_json_error(__('The image file exists; refusing to delete a valid attachment.', 'metasync')); | |
| 3229 | + } | |
| 3230 | + | |
| 3231 | + $deleted = wp_delete_attachment($attachment_id, true); | |
| 3232 | + | |
| 3233 | + if ($deleted) { | |
| 3234 | + wp_send_json_success([ | |
| 3235 | + 'stats' => Metasync_Media_Library_List_Table::get_stats(), | |
| 3236 | + ]); | |
| 3237 | + } | |
| 3238 | + | |
| 3239 | + wp_send_json_error(__('Failed to delete the orphaned media record.', 'metasync')); | |
| 3240 | + } | |
| 3241 | + | |
| 3242 | + /** | |
| 2925 | 3243 | * Cron handler: Process batch optimization tick. |
| 2926 | 3244 | */ |
| 2927 | 3245 | public function handle_media_batch_cron() |
| 2928 | 3246 | { |
| @@ -2932,8 +3250,86 @@ | ||
| 2932 | 3250 | Metasync_Media_Batch_Optimizer::process_batch_tick(); |
| 2933 | 3251 | } |
| 2934 | 3252 | |
| 2935 | 3253 | /** |
| 3254 | + * AJAX: Start SEO Restore batch. | |
| 3255 | + */ | |
| 3256 | + public function ajax_seo_restore_start() | |
| 3257 | + { | |
| 3258 | + check_ajax_referer('metasync_seo_restore_nonce', 'nonce'); | |
| 3259 | + | |
| 3260 | + if (!current_user_can('manage_options')) { | |
| 3261 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3262 | + } | |
| 3263 | + | |
| 3264 | + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php'; | |
| 3265 | + | |
| 3266 | + $progress = Metasync_Seo_Restore::start_batch(); | |
| 3267 | + wp_send_json_success($progress); | |
| 3268 | + } | |
| 3269 | + | |
| 3270 | + /** | |
| 3271 | + * AJAX: Cancel SEO Restore batch. | |
| 3272 | + */ | |
| 3273 | + public function ajax_seo_restore_cancel() | |
| 3274 | + { | |
| 3275 | + check_ajax_referer('metasync_seo_restore_nonce', 'nonce'); | |
| 3276 | + | |
| 3277 | + if (!current_user_can('manage_options')) { | |
| 3278 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3279 | + } | |
| 3280 | + | |
| 3281 | + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php'; | |
| 3282 | + | |
| 3283 | + Metasync_Seo_Restore::cancel_batch(); | |
| 3284 | + wp_send_json_success(); | |
| 3285 | + } | |
| 3286 | + | |
| 3287 | + /** | |
| 3288 | + * AJAX: Get SEO Restore batch progress. | |
| 3289 | + */ | |
| 3290 | + public function ajax_seo_restore_progress() | |
| 3291 | + { | |
| 3292 | + check_ajax_referer('metasync_seo_restore_nonce', 'nonce'); | |
| 3293 | + | |
| 3294 | + if (!current_user_can('manage_options')) { | |
| 3295 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3296 | + } | |
| 3297 | + | |
| 3298 | + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php'; | |
| 3299 | + | |
| 3300 | + $progress = Metasync_Seo_Restore::get_progress(); | |
| 3301 | + wp_send_json_success($progress); | |
| 3302 | + } | |
| 3303 | + | |
| 3304 | + /** | |
| 3305 | + * AJAX: Process one SEO Restore tick (browser-driven chaining). | |
| 3306 | + */ | |
| 3307 | + public function ajax_seo_restore_process_tick() | |
| 3308 | + { | |
| 3309 | + check_ajax_referer('metasync_seo_restore_nonce', 'nonce'); | |
| 3310 | + | |
| 3311 | + if (!current_user_can('manage_options')) { | |
| 3312 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3313 | + } | |
| 3314 | + | |
| 3315 | + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php'; | |
| 3316 | + | |
| 3317 | + $progress = Metasync_Seo_Restore::process_ajax_tick(); | |
| 3318 | + wp_send_json_success($progress); | |
| 3319 | + } | |
| 3320 | + | |
| 3321 | + /** | |
| 3322 | + * Cron handler: Process SEO Restore batch tick. | |
| 3323 | + */ | |
| 3324 | + public function handle_seo_restore_cron() | |
| 3325 | + { | |
| 3326 | + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php'; | |
| 3327 | + | |
| 3328 | + Metasync_Seo_Restore::process_batch_tick(); | |
| 3329 | + } | |
| 3330 | + | |
| 3331 | + /** | |
| 2936 | 3332 | * Report Issue page callback |
| 2937 | 3333 | */ |
| 2938 | 3334 | public function create_admin_report_issue_page() |
| 2939 | 3335 | { |
| @@ -2979,8 +3375,10 @@ | ||
| 2979 | 3375 | if (is_wp_error($result)) { |
| 2980 | 3376 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 2981 | 3377 | sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message()) |
| 2982 | 3378 | ) . '</p></div>'; |
| 3379 | + } elseif (false === $result) { | |
| 3380 | + echo '<div class="notice notice-error"><p>' . esc_html__('Settings saved but sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '</p></div>'; | |
| 2983 | 3381 | } else { |
| 2984 | 3382 | echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '</p></div>'; |
| 2985 | 3383 | } |
| 2986 | 3384 | } |
| @@ -3043,8 +3441,12 @@ | ||
| 3043 | 3441 | 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])), |
| 3044 | 3442 | 'auto_detect' => isset($_POST['auto_detect']), |
| 3045 | 3443 | 'taxonomies' => $video_taxonomies, |
| 3046 | 3444 | 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['video_excluded_urls'] ?? '')), |
| 3445 | + // This array replaces the stored option wholesale, so every | |
| 3446 | + // setting must be represented here or it is wiped on save. | |
| 3447 | + 'video_url_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_url_meta_keys'] ?? '')), | |
| 3448 | + 'video_thumbnail_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_thumbnail_meta_keys'] ?? '')), | |
| 3047 | 3449 | ]; |
| 3048 | 3450 | |
| 3049 | 3451 | // Always invalidate old cache before saving new settings |
| 3050 | 3452 | delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); |
| @@ -3102,8 +3504,17 @@ | ||
| 3102 | 3504 | if ($sitemap_generator->generate_news_sitemap()) { |
| 3103 | 3505 | $extras[] = 'news'; |
| 3104 | 3506 | } |
| 3105 | 3507 | } |
| 3508 | + | |
| 3509 | + // Reclaim unreferenced memory between the news and video passes | |
| 3510 | + // so the video generator starts with more headroom (they share | |
| 3511 | + // one request). Each generator already releases its own object | |
| 3512 | + // cache via clean_post_cache(), so no cache flush is needed here. | |
| 3513 | + if (function_exists('gc_collect_cycles')) { | |
| 3514 | + gc_collect_cycles(); | |
| 3515 | + } | |
| 3516 | + | |
| 3106 | 3517 | if (!empty($video_opts['enabled'])) { |
| 3107 | 3518 | if ($sitemap_generator->generate_video_sitemap()) { |
| 3108 | 3519 | $extras[] = 'video'; |
| 3109 | 3520 | } |
| @@ -3108,8 +3519,13 @@ | ||
| 3108 | 3519 | $extras[] = 'video'; |
| 3109 | 3520 | } |
| 3110 | 3521 | } |
| 3111 | 3522 | |
| 3523 | + // Same inter-pass GC before the main sitemap pass begins. | |
| 3524 | + if (function_exists('gc_collect_cycles')) { | |
| 3525 | + gc_collect_cycles(); | |
| 3526 | + } | |
| 3527 | + | |
| 3112 | 3528 | // Generate main sitemap (its index will include news/video since they now exist) |
| 3113 | 3529 | $result = $sitemap_generator->generate_sitemap(); |
| 3114 | 3530 | |
| 3115 | 3531 | if (is_wp_error($result)) { |
| @@ -3117,8 +3533,10 @@ | ||
| 3117 | 3533 | error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); |
| 3118 | 3534 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 3119 | 3535 | sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg) |
| 3120 | 3536 | ) . '</p></div>'; |
| 3537 | + } elseif (false === $result) { | |
| 3538 | + echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '</p></div>'; | |
| 3121 | 3539 | } else { |
| 3122 | 3540 | $message = esc_html__('Sitemap generated successfully!', 'metasync'); |
| 3123 | 3541 | if (!empty($extras)) { |
| 3124 | 3542 | $message .= ' ' . sprintf( |
| @@ -3157,9 +3575,9 @@ | ||
| 3157 | 3575 | |
| 3158 | 3576 | if ($deleted) { |
| 3159 | 3577 | // Disable auto-update only when the general sitemap is removed |
| 3160 | 3578 | update_option('metasync_sitemap_auto_update', false); |
| 3161 | - // Re-enable WP core sitemap only if no other MetaSync sitemaps remain (WP-396) | |
| 3579 | + // Re-enable WP core sitemap only if no other MetaSync sitemaps remain | |
| 3162 | 3580 | if (!$sitemap_generator->sitemap_exists()) { |
| 3163 | 3581 | delete_option('metasync_disable_wp_sitemap'); |
| 3164 | 3582 | } |
| 3165 | 3583 | echo '<div class="notice notice-success"><p>' . esc_html__('General sitemap deleted successfully!', 'metasync') . '</p></div>'; |
| @@ -3188,9 +3606,9 @@ | ||
| 3188 | 3606 | |
| 3189 | 3607 | if ($deleted) { |
| 3190 | 3608 | // Also disable auto-update when deleting |
| 3191 | 3609 | update_option('metasync_sitemap_auto_update', false); |
| 3192 | - // Re-enable WP core sitemap so the site isn't left with zero sitemaps (WP-396) | |
| 3610 | + // Re-enable WP core sitemap so the site isn't left with zero sitemaps | |
| 3193 | 3611 | delete_option('metasync_disable_wp_sitemap'); |
| 3194 | 3612 | echo '<div class="notice notice-success"><p>' . esc_html__('All sitemaps deleted successfully!', 'metasync') . '</p></div>'; |
| 3195 | 3613 | } else { |
| 3196 | 3614 | echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemaps. The files may not exist or are not writable.', 'metasync') . '</p></div>'; |
| @@ -3311,47 +3729,8 @@ | ||
| 3311 | 3729 | Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page(); |
| 3312 | 3730 | } |
| 3313 | 3731 | |
| 3314 | 3732 | /** |
| 3315 | - * Google Instant Index Setting page callback | |
| 3316 | - */ | |
| 3317 | - public function create_admin_google_instant_index_page() | |
| 3318 | - { | |
| 3319 | - $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.'); | |
| 3320 | - | |
| 3321 | - // Render shared Google Index credentials section | |
| 3322 | - if (!function_exists('google_index_direct')) { | |
| 3323 | - if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) { | |
| 3324 | - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 3325 | - } else { | |
| 3326 | - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); | |
| 3327 | - return; | |
| 3328 | - } | |
| 3329 | - } | |
| 3330 | - $google_index = google_index_direct(); | |
| 3331 | - $service_info = $google_index->get_service_account_info(); | |
| 3332 | - $is_configured = !isset($service_info['error']); | |
| 3333 | - | |
| 3334 | - $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : ''; | |
| 3335 | - | |
| 3336 | - include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php'; | |
| 3337 | - | |
| 3338 | - // Render post types selection with save form | |
| 3339 | - $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); | |
| 3340 | - $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; | |
| 3341 | - ?> | |
| 3342 | - <form method="POST" action=""> | |
| 3343 | - <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?> | |
| 3344 | - <div class="dashboard-card" style="padding: 20px;"> | |
| 3345 | - <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?> | |
| 3346 | - </div> | |
| 3347 | - </form> | |
| 3348 | - <?php | |
| 3349 | - | |
| 3350 | - $this->render_layout_close(); | |
| 3351 | - } | |
| 3352 | - | |
| 3353 | - /** | |
| 3354 | 3733 | * Google Console page callback |
| 3355 | 3734 | */ |
| 3356 | 3735 | public function create_admin_google_console_page() |
| 3357 | 3736 | { |
| @@ -3374,16 +3753,8 @@ | ||
| 3374 | 3753 | $this->render_layout_close(); |
| 3375 | 3754 | } |
| 3376 | 3755 | |
| 3377 | 3756 | /** |
| 3378 | - * General Options page callback | |
| 3379 | - */ | |
| 3380 | - public function create_admin_optimal_settings_page() | |
| 3381 | - { | |
| 3382 | - Metasync_Admin_Pages::get_instance($this)->create_admin_optimal_settings_page(); | |
| 3383 | - } | |
| 3384 | - | |
| 3385 | - /** | |
| 3386 | 3757 | * Global Options page callback |
| 3387 | 3758 | */ |
| 3388 | 3759 | public function create_admin_global_settings_page() |
| 3389 | 3760 | { |
| @@ -3415,16 +3786,8 @@ | ||
| 3415 | 3786 | Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page(); |
| 3416 | 3787 | } |
| 3417 | 3788 | |
| 3418 | 3789 | /** |
| 3419 | - * Site Optimal Settings page callback | |
| 3420 | - */ | |
| 3421 | - public function optimization_settings_options() | |
| 3422 | - { | |
| 3423 | - Metasync_Admin_Pages::get_instance($this)->optimization_settings_options(); | |
| 3424 | - } | |
| 3425 | - | |
| 3426 | - /** | |
| 3427 | 3790 | * redirection page callback with tabs |
| 3428 | 3791 | */ |
| 3429 | 3792 | public function create_admin_redirections_page() |
| 3430 | 3793 | { |
| @@ -3451,10 +3814,11 @@ | ||
| 3451 | 3814 | } |
| 3452 | 3815 | delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT ); |
| 3453 | 3816 | echo '<div class="notice notice-warning is-dismissible"><p>'; |
| 3454 | 3817 | printf( |
| 3455 | - /* translators: 1: current load, 2: threshold, 3: core count */ | |
| 3456 | - esc_html__( 'MetaSync: Batch processing was deferred — server CPU load (%1$s) exceeded the threshold (%2$s on %3$s cores). Processing will resume automatically.', 'metasync' ), | |
| 3818 | + /* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */ | |
| 3819 | + esc_html__( '%1$s: Batch processing was deferred — server CPU load (%2$s) exceeded the threshold (%3$s on %4$s cores). Processing will resume automatically.', 'metasync' ), | |
| 3820 | + esc_html( Metasync::get_effective_plugin_name() ), | |
| 3457 | 3821 | esc_html( $data['load'] ), |
| 3458 | 3822 | esc_html( $data['threshold'] ), |
| 3459 | 3823 | esc_html( $data['cores'] ) |
| 3460 | 3824 | ); |
| @@ -3472,9 +3836,9 @@ | ||
| 3472 | 3836 | if (!get_transient('metasync_llms_conflict')) { |
| 3473 | 3837 | return; |
| 3474 | 3838 | } |
| 3475 | 3839 | echo '<div class="notice notice-info is-dismissible"><p>'; |
| 3476 | - echo esc_html__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. MetaSync\'s version takes priority when enabled.', 'metasync'); | |
| 3840 | + echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name())); | |
| 3477 | 3841 | echo '</p></div>'; |
| 3478 | 3842 | } |
| 3479 | 3843 | |
| 3480 | 3844 | /** |
| @@ -3543,8 +3907,17 @@ | ||
| 3543 | 3907 | public function ajax_restore_robots_backup() |
| 3544 | 3908 | { |
| 3545 | 3909 | Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup(); |
| 3546 | 3910 | } |
| 3911 | + | |
| 3912 | + /** | |
| 3913 | + * AJAX handler to fetch a paginated page of robots.txt backups | |
| 3914 | + */ | |
| 3915 | + public function ajax_get_robots_backups() | |
| 3916 | + { | |
| 3917 | + Metasync_Admin_Ajax::instance()->ajax_get_robots_backups(); | |
| 3918 | + } | |
| 3919 | + | |
| 3547 | 3920 | public function ajax_create_redirect_from_404() |
| 3548 | 3921 | { |
| 3549 | 3922 | Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404(); |
| 3550 | 3923 | } |
| @@ -3657,9 +4030,9 @@ | ||
| 3657 | 4030 | } |
| 3658 | 4031 | |
| 3659 | 4032 | // Get pagination parameters |
| 3660 | 4033 | $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; |
| 3661 | - $per_page = 10; | |
| 4034 | + $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10); | |
| 3662 | 4035 | $offset = ($page - 1) * $per_page; |
| 3663 | 4036 | |
| 3664 | 4037 | // Get filters |
| 3665 | 4038 | $filters = [ |
| @@ -3818,14 +4191,15 @@ | ||
| 3818 | 4191 | <?php endif; ?> |
| 3819 | 4192 | </div> |
| 3820 | 4193 | |
| 3821 | 4194 | <!-- Pagination --> |
| 3822 | - <?php if ($total_pages > 1): ?> | |
| 3823 | - <div class="sync-log-pagination"> | |
| 3824 | - <div class="sync-log-pagination-info"> | |
| 3825 | - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?> | |
| 3826 | - </div> | |
| 4195 | + <div class="sync-log-pagination"> | |
| 4196 | + <div class="sync-log-pagination-info"> | |
| 4197 | + Total records: <?php echo intval( $total_records ); ?><?php if ($total_records > 0): ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?><?php endif; ?> | |
| 4198 | + <?php echo Metasync_Per_Page_Helper::render_selector('sync_log', $per_page); ?> | |
| 4199 | + </div> | |
| 3827 | 4200 | |
| 4201 | + <?php if ($total_pages > 1): ?> | |
| 3828 | 4202 | <div class="sync-log-pagination-controls"> |
| 3829 | 4203 | <?php if ($page > 1): ?> |
| 3830 | 4204 | <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">‹</a> |
| 3831 | 4205 | <?php endif; ?> |
| @@ -3838,10 +4212,10 @@ | ||
| 3838 | 4212 | <?php if ($page < $total_pages): ?> |
| 3839 | 4213 | <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">›</a> |
| 3840 | 4214 | <?php endif; ?> |
| 3841 | 4215 | </div> |
| 3842 | - </div> | |
| 3843 | - <?php endif; ?> | |
| 4216 | + <?php endif; ?> | |
| 4217 | + </div> | |
| 3844 | 4218 | </div> |
| 3845 | 4219 | <?php $this->render_layout_close(); ?> |
| 3846 | 4220 | |
| 3847 | 4221 | <script> |
| @@ -3922,8 +4296,19 @@ | ||
| 3922 | 4296 | if (!empty($value)) { |
| 3923 | 4297 | $query_parts[] = $key . '=' . urlencode($value); |
| 3924 | 4298 | } |
| 3925 | 4299 | } |
| 4300 | + | |
| 4301 | + // Preserve the user-selected results-per-page value across page | |
| 4302 | + // navigation so a non-default page size survives clicking a page link. | |
| 4303 | + $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log'); | |
| 4304 | + if (isset($_GET[$per_page_key])) { | |
| 4305 | + $per_page = (int) $_GET[$per_page_key]; | |
| 4306 | + if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) { | |
| 4307 | + $query_parts[] = $per_page_key . '=' . $per_page; | |
| 4308 | + } | |
| 4309 | + } | |
| 4310 | + | |
| 3926 | 4311 | return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; |
| 3927 | 4312 | } |
| 3928 | 4313 | |
| 3929 | 4314 | /** |
| @@ -4154,8 +4539,9 @@ | ||
| 4154 | 4539 | $stats = Metasync_CPU_Monitor::get_stats(); |
| 4155 | 4540 | $per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold(); |
| 4156 | 4541 | $cores = Metasync_CPU_Monitor::get_cpu_core_count(); |
| 4157 | 4542 | $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold(); |
| 4543 | + $detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable(); | |
| 4158 | 4544 | ?> |
| 4159 | 4545 | <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;"> |
| 4160 | 4546 | <!-- CPU Cores Detected --> |
| 4161 | 4547 | <div style="margin-bottom: 24px;"> |
| @@ -4162,12 +4548,20 @@ | ||
| 4162 | 4548 | <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 4163 | 4549 | CPU Cores Detected |
| 4164 | 4550 | </label> |
| 4165 | 4551 | <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);"> |
| 4166 | - <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?> | |
| 4552 | + <?php if ($detection_reliable) : ?> | |
| 4553 | + <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?> | |
| 4554 | + <?php else : ?> | |
| 4555 | + <strong>Not detected</strong> | |
| 4556 | + <?php endif; ?> | |
| 4167 | 4557 | </div> |
| 4168 | 4558 | <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);"> |
| 4169 | - Automatically detected on this system. | |
| 4559 | + <?php if ($detection_reliable) : ?> | |
| 4560 | + Automatically detected on this system. | |
| 4561 | + <?php else : ?> | |
| 4562 | + Core detection is not available on this hosting environment. | |
| 4563 | + <?php endif; ?> | |
| 4170 | 4564 | </p> |
| 4171 | 4565 | </div> |
| 4172 | 4566 | |
| 4173 | 4567 | <!-- Per-Core Load Threshold --> |
| @@ -4173,8 +4567,9 @@ | ||
| 4173 | 4567 | <!-- Per-Core Load Threshold --> |
| 4174 | 4568 | <div style="margin-bottom: 24px;"> |
| 4175 | 4569 | <label for="cpu_load_per_core_threshold" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 4176 | 4570 | Per-Core Load Threshold |
| 4571 | + <?php Metasync::render_tooltip_icon('cpu_per_core_load_threshold', 'How busy each CPU core can get before the plugin pauses background SEO syncing. Lower = more cautious. Most sites leave this at 2.0.'); ?> | |
| 4177 | 4572 | </label> |
| 4178 | 4573 | <input type="number" |
| 4179 | 4574 | id="cpu_load_per_core_threshold" |
| 4180 | 4575 | name="metasync_options[performance][cpu_load_per_core_threshold]" |
| @@ -4726,88 +5121,8 @@ | ||
| 4726 | 5121 | |
| 4727 | 5122 | /** |
| 4728 | 5123 | * Get the settings option array and print one of its values |
| 4729 | 5124 | */ |
| 4730 | - public function no_index_posts_callback() | |
| 4731 | - { | |
| 4732 | - Metasync_Settings_Fields::instance()->no_index_posts_callback(); | |
| 4733 | - } | |
| 4734 | - | |
| 4735 | - /** | |
| 4736 | - * Get the settings option array and print one of its values | |
| 4737 | - */ | |
| 4738 | - public function no_follow_links_callback() | |
| 4739 | - { | |
| 4740 | - Metasync_Settings_Fields::instance()->no_follow_links_callback(); | |
| 4741 | - } | |
| 4742 | - | |
| 4743 | - /** | |
| 4744 | - * Get the settings option array and print one of its values | |
| 4745 | - */ | |
| 4746 | - public function open_external_links_callback() | |
| 4747 | - { | |
| 4748 | - Metasync_Settings_Fields::instance()->open_external_links_callback(); | |
| 4749 | - } | |
| 4750 | - | |
| 4751 | - /** | |
| 4752 | - * Get the settings option array and print one of its values | |
| 4753 | - */ | |
| 4754 | - public function add_alt_image_tags_callback() | |
| 4755 | - { | |
| 4756 | - Metasync_Settings_Fields::instance()->add_alt_image_tags_callback(); | |
| 4757 | - } | |
| 4758 | - | |
| 4759 | - /** | |
| 4760 | - * Get the settings option array and print one of its values | |
| 4761 | - */ | |
| 4762 | - public function add_title_image_tags_callback() | |
| 4763 | - { | |
| 4764 | - Metasync_Settings_Fields::instance()->add_title_image_tags_callback(); | |
| 4765 | - } | |
| 4766 | - | |
| 4767 | - /** | |
| 4768 | - * Get the settings option array and print one of its values | |
| 4769 | - */ | |
| 4770 | - public function site_type_callback() | |
| 4771 | - { | |
| 4772 | - Metasync_Settings_Fields::instance()->site_type_callback(); | |
| 4773 | - } | |
| 4774 | - | |
| 4775 | - /** | |
| 4776 | - * Get the settings option array and print one of its values | |
| 4777 | - */ | |
| 4778 | - public function site_business_type_callback() | |
| 4779 | - { | |
| 4780 | - Metasync_Settings_Fields::instance()->site_business_type_callback(); | |
| 4781 | - } | |
| 4782 | - | |
| 4783 | - /** | |
| 4784 | - * Get the settings option array and print one of its values | |
| 4785 | - */ | |
| 4786 | - public function site_company_name_callback() | |
| 4787 | - { | |
| 4788 | - Metasync_Settings_Fields::instance()->site_company_name_callback(); | |
| 4789 | - } | |
| 4790 | - | |
| 4791 | - /** | |
| 4792 | - * Get the settings option array and print one of its values | |
| 4793 | - */ | |
| 4794 | - public function site_google_logo_callback() | |
| 4795 | - { | |
| 4796 | - Metasync_Settings_Fields::instance()->site_google_logo_callback(); | |
| 4797 | - } | |
| 4798 | - | |
| 4799 | - /** | |
| 4800 | - * Get the settings option array and print one of its values | |
| 4801 | - */ | |
| 4802 | - public function site_social_share_image_callback() | |
| 4803 | - { | |
| 4804 | - Metasync_Settings_Fields::instance()->site_social_share_image_callback(); | |
| 4805 | - } | |
| 4806 | - | |
| 4807 | - /** | |
| 4808 | - * Get the settings option array and print one of its values | |
| 4809 | - */ | |
| 4810 | 5125 | public function common_robot_meta_tags_callback() |
| 4811 | 5126 | { |
| 4812 | 5127 | Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback(); |
| 4813 | 5128 | } |
| @@ -5005,8 +5320,14 @@ | ||
| 5005 | 5320 | * Show a one-time admin notice when a page builder is detected but the |
| 5006 | 5321 | * "Default Page Builder" setting has never been explicitly saved. |
| 5007 | 5322 | */ |
| 5008 | 5323 | public function display_page_builder_notice() { |
| 5324 | + // Only relevant on the plugin's own settings page — avoid repeating | |
| 5325 | + // this notice across every admin screen. | |
| 5326 | + if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) { | |
| 5327 | + return; | |
| 5328 | + } | |
| 5329 | + | |
| 5009 | 5330 | $configured = Metasync::get_option('general')['default_page_builder'] ?? ''; |
| 5010 | 5331 | |
| 5011 | 5332 | // Setting already saved — nothing to warn about |
| 5012 | 5333 | if (!empty($configured)) { |
| @@ -5974,8 +6295,16 @@ | ||
| 5974 | 6295 | if (!$post || !in_array($post->post_type, array('post', 'page'))) { |
| 5975 | 6296 | return; |
| 5976 | 6297 | } |
| 5977 | 6298 | |
| 6299 | + // Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML | |
| 6300 | + // pages: those are raw-HTML store-and-serve pages, NOT actually converted to a | |
| 6301 | + // page builder, so the banner mislabels them. It still shows for pages genuinely | |
| 6302 | + // produced by the converter. | |
| 6303 | + if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) { | |
| 6304 | + return; | |
| 6305 | + } | |
| 6306 | + | |
| 5978 | 6307 | $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true); |
| 5979 | 6308 | $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true); |
| 5980 | 6309 | |
| 5981 | 6310 | if ($has_raw_html || !empty($has_custom_css)) { |
| @@ -6188,8 +6517,18 @@ | ||
| 6188 | 6517 | if (!isset($_POST['submit'])) { |
| 6189 | 6518 | return; |
| 6190 | 6519 | } |
| 6191 | 6520 | |
| 6521 | + // This handler runs on admin_init, which also fires inside | |
| 6522 | + // admin-ajax.php before its login gate, so a bare isset() check let | |
| 6523 | + // any request (including logged-out ones) rewrite the auto-submit | |
| 6524 | + // post types. Require the nonce and plugin access before writing. | |
| 6525 | + if (!isset($_POST['metasync_instant_indexing_nonce']) | |
| 6526 | + || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['metasync_instant_indexing_nonce'])), 'metasync_instant_indexing_settings') | |
| 6527 | + || !Metasync::current_user_has_plugin_access()) { | |
| 6528 | + return; | |
| 6529 | + } | |
| 6530 | + | |
| 6192 | 6531 | // Save post types for Google Instant Indexing auto-submit |
| 6193 | 6532 | if (isset($_POST['metasync_post_types'])) { |
| 6194 | 6533 | $post_data = metasync_sanitize_input_array($_POST); |
| 6195 | 6534 | $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : []; |
| @@ -6273,12 +6612,24 @@ | ||
| 6273 | 6612 | if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') { |
| 6274 | 6613 | $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); |
| 6275 | 6614 | $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; |
| 6276 | 6615 | |
| 6277 | - if (in_array($post->post_type, $post_types) && function_exists('google_index_direct')) { | |
| 6278 | - $service_info = google_index_direct()->get_service_account_info(); | |
| 6279 | - if (!isset($service_info['error'])) { | |
| 6280 | - google_index_direct()->index_post($post_id, $post->post_type, 'update'); | |
| 6616 | + if (in_array($post->post_type, $post_types)) { | |
| 6617 | + // save_post is the single owner of auto-submit and also fires | |
| 6618 | + // on REST-created posts, where nothing else may have loaded | |
| 6619 | + // the Google Index helpers yet — load them on demand instead | |
| 6620 | + // of silently skipping. | |
| 6621 | + if (!function_exists('google_index_direct')) { | |
| 6622 | + $google_index_path = plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 6623 | + if (file_exists($google_index_path)) { | |
| 6624 | + require_once $google_index_path; | |
| 6625 | + } | |
| 6626 | + } | |
| 6627 | + if (function_exists('google_index_direct')) { | |
| 6628 | + $service_info = google_index_direct()->get_service_account_info(); | |
| 6629 | + if (!isset($service_info['error'])) { | |
| 6630 | + google_index_direct()->index_post($post_id, $post->post_type, 'update'); | |
| 6631 | + } | |
| 6281 | 6632 | } |
| 6282 | 6633 | } |
| 6283 | 6634 | } |
| 6284 | 6635 | |