| @@ -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. |
| @@ -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')); |
| @@ -373,8 +376,15 @@ | ||
| 373 | 376 | add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick')); |
| 374 | 377 | add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image')); |
| 375 | 378 | add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron')); |
| 376 | 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 | + | |
| 377 | 387 | # Add AJAX handlers for Google Instant Indexing |
| 378 | 388 | add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi')); |
| 379 | 389 | |
| 380 | 390 | # Add AJAX handlers for Bing Instant Indexing (IndexNow) |
| @@ -708,11 +718,56 @@ | ||
| 708 | 718 | exit; |
| 709 | 719 | } |
| 710 | 720 | } |
| 711 | 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 | + | |
| 712 | 766 | public function metasync_display_error_log() { |
| 713 | 767 | Metasync_Debug_Manager::instance()->metasync_display_error_log($this); |
| 714 | 768 | } |
| 769 | + | |
| 715 | 770 | public function metasync_update_wp_config() { |
| 716 | 771 | Metasync_Debug_Manager::instance()->metasync_update_wp_config(); |
| 717 | 772 | } |
| 718 | 773 | |
| @@ -970,10 +1025,41 @@ | ||
| 970 | 1025 | array('jquery'), |
| 971 | 1026 | $this->version, |
| 972 | 1027 | true |
| 973 | 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 | + ); | |
| 974 | 1040 | } |
| 975 | 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 | + | |
| 976 | 1062 | // Dashboard iframe height (only on dashboard page) |
| 977 | 1063 | if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) { |
| 978 | 1064 | wp_enqueue_script( |
| 979 | 1065 | $this->plugin_name . '-iframe', |
| @@ -1446,14 +1532,44 @@ | ||
| 1446 | 1532 | } |
| 1447 | 1533 | |
| 1448 | 1534 | /** |
| 1449 | 1535 | * Settings of HeartBeat API for admin area. |
| 1450 | - * 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. | |
| 1451 | 1554 | */ |
| 1452 | 1555 | function metasync_heartbeat_settings($settings) |
| 1453 | 1556 | { |
| 1454 | - global $heartbeat_frequency; | |
| 1455 | - $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; | |
| 1456 | 1572 | return $settings; |
| 1457 | 1573 | } |
| 1458 | 1574 | |
| 1459 | 1575 | /** |
| @@ -1626,9 +1742,9 @@ | ||
| 1626 | 1742 | <!-- OTTO Cache TTL Setting --> |
| 1627 | 1743 | <div style="margin-bottom: 30px; padding-top: 20px;"> |
| 1628 | 1744 | <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Cache TTL</h4> |
| 1629 | 1745 | <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);"> |
| 1630 | - 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. | |
| 1631 | 1747 | </p> |
| 1632 | 1748 | |
| 1633 | 1749 | <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 1634 | 1750 | <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;"> |
| @@ -1634,8 +1750,9 @@ | ||
| 1634 | 1750 | <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;"> |
| 1635 | 1751 | <label for="metasync-otto-cache-ttl" style="color: var(--dashboard-text-primary); font-weight: 500;"> |
| 1636 | 1752 | Cache TTL (minutes): |
| 1637 | 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.'); ?> | |
| 1638 | 1755 | <input type="number" |
| 1639 | 1756 | id="metasync-otto-cache-ttl" |
| 1640 | 1757 | value="<?php echo esc_attr($this->get_otto_cache_ttl_minutes()); ?>" |
| 1641 | 1758 | min="30" |
| @@ -1779,8 +1896,9 @@ | ||
| 1779 | 1896 | <?php checked(true, !empty($hosting_settings['wpengine_enabled'])); ?> |
| 1780 | 1897 | <?php echo !$wpe_detected ? 'disabled' : ''; ?> |
| 1781 | 1898 | style="width: 16px; height: 16px; cursor: <?php echo $wpe_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1782 | 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.'); ?> | |
| 1783 | 1901 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges Varnish + Memcached</span> |
| 1784 | 1902 | </label> |
| 1785 | 1903 | |
| 1786 | 1904 | <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 16px; cursor: pointer;"> |
| @@ -1789,8 +1907,9 @@ | ||
| 1789 | 1907 | <?php checked(true, !empty($hosting_settings['kinsta_enabled'])); ?> |
| 1790 | 1908 | <?php echo !$kinsta_detected ? 'disabled' : ''; ?> |
| 1791 | 1909 | style="width: 16px; height: 16px; cursor: <?php echo $kinsta_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1792 | 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.'); ?> | |
| 1793 | 1912 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges full-page cache (kinsta_cache_purge_full)</span> |
| 1794 | 1913 | </label> |
| 1795 | 1914 | |
| 1796 | 1915 | <div style="display: flex; align-items: center; gap: 12px;"> |
| @@ -1938,9 +2057,9 @@ | ||
| 1938 | 2057 | id="metasync-targeted-object-cache" |
| 1939 | 2058 | <?php checked('1', $targeted_cache_enabled); ?> |
| 1940 | 2059 | style="width: 16px; height: 16px; cursor: pointer; margin-top: 2px; flex-shrink: 0;" /> |
| 1941 | 2060 | <span> |
| 1942 | - <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> | |
| 1943 | 2062 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;"> |
| 1944 | 2063 | When enabled, only the updated posts are evicted from the object cache (recommended for large sites). |
| 1945 | 2064 | When disabled, a full <code>wp_cache_flush()</code> is used instead. |
| 1946 | 2065 | </span> |
| @@ -2705,8 +2824,15 @@ | ||
| 2705 | 2824 | // Handle form submissions |
| 2706 | 2825 | if (isset($_POST['metasync_media_optimization_nonce'])) { |
| 2707 | 2826 | check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce'); |
| 2708 | 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 | + | |
| 2709 | 2835 | // Handle reset to defaults |
| 2710 | 2836 | if (!empty($_POST['metasync_media_reset'])) { |
| 2711 | 2837 | $defaults = Metasync_Media_Settings::get_defaults(); |
| 2712 | 2838 | Metasync_Media_Settings::save_settings($defaults); |
| @@ -2796,8 +2922,15 @@ | ||
| 2796 | 2922 | if (!$attachment_id) { |
| 2797 | 2923 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2798 | 2924 | } |
| 2799 | 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 | + | |
| 2800 | 2933 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; |
| 2801 | 2934 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php'; |
| 2802 | 2935 | $settings = Metasync_Media_Settings::get_settings(); |
| 2803 | 2936 | |
| @@ -2849,8 +2982,20 @@ | ||
| 2849 | 2982 | if (!$attachment_id) { |
| 2850 | 2983 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2851 | 2984 | } |
| 2852 | 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 | + | |
| 2853 | 2998 | $success = Metasync_Image_Converter::revert_attachment($attachment_id); |
| 2854 | 2999 | |
| 2855 | 3000 | if ($success) { |
| 2856 | 3001 | wp_send_json_success(); |
| @@ -2902,8 +3047,15 @@ | ||
| 2902 | 3047 | public function ajax_batch_progress() |
| 2903 | 3048 | { |
| 2904 | 3049 | check_ajax_referer('metasync_media_opt_nonce', 'nonce'); |
| 2905 | 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 | + | |
| 2906 | 3058 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; |
| 2907 | 3059 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; |
| 2908 | 3060 | |
| 2909 | 3061 | $progress = Metasync_Media_Batch_Optimizer::get_progress(); |
| @@ -2934,10 +3086,19 @@ | ||
| 2934 | 3086 | $settings = Metasync_Media_Settings::get_settings(); |
| 2935 | 3087 | |
| 2936 | 3088 | $success = 0; |
| 2937 | 3089 | $failed = 0; |
| 3090 | + $denied = 0; | |
| 2938 | 3091 | |
| 2939 | 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 | + | |
| 2940 | 3101 | if (Metasync_Image_Converter::convert_attachment($id, $settings)) { |
| 2941 | 3102 | $success++; |
| 2942 | 3103 | } else { |
| 2943 | 3104 | $failed++; |
| @@ -2946,8 +3107,9 @@ | ||
| 2946 | 3107 | |
| 2947 | 3108 | wp_send_json_success([ |
| 2948 | 3109 | 'success' => $success, |
| 2949 | 3110 | 'failed' => $failed, |
| 3111 | + 'denied' => $denied, | |
| 2950 | 3112 | ]); |
| 2951 | 3113 | } |
| 2952 | 3114 | |
| 2953 | 3115 | /** |
| @@ -2970,11 +3132,19 @@ | ||
| 2970 | 3132 | |
| 2971 | 3133 | $success = 0; |
| 2972 | 3134 | $failed = 0; |
| 2973 | 3135 | $skipped = 0; |
| 3136 | + $denied = 0; | |
| 2974 | 3137 | $errors = []; |
| 2975 | 3138 | |
| 2976 | 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 | + | |
| 2977 | 3147 | $format = get_post_meta($id, '_metasync_converted_format', true); |
| 2978 | 3148 | |
| 2979 | 3149 | if (!$format) { |
| 2980 | 3150 | $skipped++; |
| @@ -3002,8 +3172,9 @@ | ||
| 3002 | 3172 | wp_send_json_success([ |
| 3003 | 3173 | 'success' => $success, |
| 3004 | 3174 | 'failed' => $failed, |
| 3005 | 3175 | 'skipped' => $skipped, |
| 3176 | + 'denied' => $denied, | |
| 3006 | 3177 | 'errors' => $errors, |
| 3007 | 3178 | ]); |
| 3008 | 3179 | } |
| 3009 | 3180 | |
| @@ -3079,8 +3250,86 @@ | ||
| 3079 | 3250 | Metasync_Media_Batch_Optimizer::process_batch_tick(); |
| 3080 | 3251 | } |
| 3081 | 3252 | |
| 3082 | 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 | + /** | |
| 3083 | 3332 | * Report Issue page callback |
| 3084 | 3333 | */ |
| 3085 | 3334 | public function create_admin_report_issue_page() |
| 3086 | 3335 | { |
| @@ -3126,8 +3375,10 @@ | ||
| 3126 | 3375 | if (is_wp_error($result)) { |
| 3127 | 3376 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 3128 | 3377 | sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message()) |
| 3129 | 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>'; | |
| 3130 | 3381 | } else { |
| 3131 | 3382 | echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '</p></div>'; |
| 3132 | 3383 | } |
| 3133 | 3384 | } |
| @@ -3190,8 +3441,12 @@ | ||
| 3190 | 3441 | 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])), |
| 3191 | 3442 | 'auto_detect' => isset($_POST['auto_detect']), |
| 3192 | 3443 | 'taxonomies' => $video_taxonomies, |
| 3193 | 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'] ?? '')), | |
| 3194 | 3449 | ]; |
| 3195 | 3450 | |
| 3196 | 3451 | // Always invalidate old cache before saving new settings |
| 3197 | 3452 | delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); |
| @@ -3249,8 +3504,17 @@ | ||
| 3249 | 3504 | if ($sitemap_generator->generate_news_sitemap()) { |
| 3250 | 3505 | $extras[] = 'news'; |
| 3251 | 3506 | } |
| 3252 | 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 | + | |
| 3253 | 3517 | if (!empty($video_opts['enabled'])) { |
| 3254 | 3518 | if ($sitemap_generator->generate_video_sitemap()) { |
| 3255 | 3519 | $extras[] = 'video'; |
| 3256 | 3520 | } |
| @@ -3255,8 +3519,13 @@ | ||
| 3255 | 3519 | $extras[] = 'video'; |
| 3256 | 3520 | } |
| 3257 | 3521 | } |
| 3258 | 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 | + | |
| 3259 | 3528 | // Generate main sitemap (its index will include news/video since they now exist) |
| 3260 | 3529 | $result = $sitemap_generator->generate_sitemap(); |
| 3261 | 3530 | |
| 3262 | 3531 | if (is_wp_error($result)) { |
| @@ -3264,8 +3533,10 @@ | ||
| 3264 | 3533 | error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); |
| 3265 | 3534 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 3266 | 3535 | sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg) |
| 3267 | 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>'; | |
| 3268 | 3539 | } else { |
| 3269 | 3540 | $message = esc_html__('Sitemap generated successfully!', 'metasync'); |
| 3270 | 3541 | if (!empty($extras)) { |
| 3271 | 3542 | $message .= ' ' . sprintf( |
| @@ -3458,47 +3729,8 @@ | ||
| 3458 | 3729 | Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page(); |
| 3459 | 3730 | } |
| 3460 | 3731 | |
| 3461 | 3732 | /** |
| 3462 | - * Google Instant Index Setting page callback | |
| 3463 | - */ | |
| 3464 | - public function create_admin_google_instant_index_page() | |
| 3465 | - { | |
| 3466 | - $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.'); | |
| 3467 | - | |
| 3468 | - // Render shared Google Index credentials section | |
| 3469 | - if (!function_exists('google_index_direct')) { | |
| 3470 | - if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) { | |
| 3471 | - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 3472 | - } else { | |
| 3473 | - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); | |
| 3474 | - return; | |
| 3475 | - } | |
| 3476 | - } | |
| 3477 | - $google_index = google_index_direct(); | |
| 3478 | - $service_info = $google_index->get_service_account_info(); | |
| 3479 | - $is_configured = !isset($service_info['error']); | |
| 3480 | - | |
| 3481 | - $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : ''; | |
| 3482 | - | |
| 3483 | - include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php'; | |
| 3484 | - | |
| 3485 | - // Render post types selection with save form | |
| 3486 | - $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); | |
| 3487 | - $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; | |
| 3488 | - ?> | |
| 3489 | - <form method="POST" action=""> | |
| 3490 | - <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?> | |
| 3491 | - <div class="dashboard-card" style="padding: 20px;"> | |
| 3492 | - <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?> | |
| 3493 | - </div> | |
| 3494 | - </form> | |
| 3495 | - <?php | |
| 3496 | - | |
| 3497 | - $this->render_layout_close(); | |
| 3498 | - } | |
| 3499 | - | |
| 3500 | - /** | |
| 3501 | 3733 | * Google Console page callback |
| 3502 | 3734 | */ |
| 3503 | 3735 | public function create_admin_google_console_page() |
| 3504 | 3736 | { |
| @@ -3521,16 +3753,8 @@ | ||
| 3521 | 3753 | $this->render_layout_close(); |
| 3522 | 3754 | } |
| 3523 | 3755 | |
| 3524 | 3756 | /** |
| 3525 | - * General Options page callback | |
| 3526 | - */ | |
| 3527 | - public function create_admin_optimal_settings_page() | |
| 3528 | - { | |
| 3529 | - Metasync_Admin_Pages::get_instance($this)->create_admin_optimal_settings_page(); | |
| 3530 | - } | |
| 3531 | - | |
| 3532 | - /** | |
| 3533 | 3757 | * Global Options page callback |
| 3534 | 3758 | */ |
| 3535 | 3759 | public function create_admin_global_settings_page() |
| 3536 | 3760 | { |
| @@ -3562,16 +3786,8 @@ | ||
| 3562 | 3786 | Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page(); |
| 3563 | 3787 | } |
| 3564 | 3788 | |
| 3565 | 3789 | /** |
| 3566 | - * Site Optimal Settings page callback | |
| 3567 | - */ | |
| 3568 | - public function optimization_settings_options() | |
| 3569 | - { | |
| 3570 | - Metasync_Admin_Pages::get_instance($this)->optimization_settings_options(); | |
| 3571 | - } | |
| 3572 | - | |
| 3573 | - /** | |
| 3574 | 3790 | * redirection page callback with tabs |
| 3575 | 3791 | */ |
| 3576 | 3792 | public function create_admin_redirections_page() |
| 3577 | 3793 | { |
| @@ -3691,8 +3907,17 @@ | ||
| 3691 | 3907 | public function ajax_restore_robots_backup() |
| 3692 | 3908 | { |
| 3693 | 3909 | Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup(); |
| 3694 | 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 | + | |
| 3695 | 3920 | public function ajax_create_redirect_from_404() |
| 3696 | 3921 | { |
| 3697 | 3922 | Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404(); |
| 3698 | 3923 | } |
| @@ -3805,9 +4030,9 @@ | ||
| 3805 | 4030 | } |
| 3806 | 4031 | |
| 3807 | 4032 | // Get pagination parameters |
| 3808 | 4033 | $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; |
| 3809 | - $per_page = 10; | |
| 4034 | + $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10); | |
| 3810 | 4035 | $offset = ($page - 1) * $per_page; |
| 3811 | 4036 | |
| 3812 | 4037 | // Get filters |
| 3813 | 4038 | $filters = [ |
| @@ -3966,14 +4191,15 @@ | ||
| 3966 | 4191 | <?php endif; ?> |
| 3967 | 4192 | </div> |
| 3968 | 4193 | |
| 3969 | 4194 | <!-- Pagination --> |
| 3970 | - <?php if ($total_pages > 1): ?> | |
| 3971 | - <div class="sync-log-pagination"> | |
| 3972 | - <div class="sync-log-pagination-info"> | |
| 3973 | - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?> | |
| 3974 | - </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> | |
| 3975 | 4200 | |
| 4201 | + <?php if ($total_pages > 1): ?> | |
| 3976 | 4202 | <div class="sync-log-pagination-controls"> |
| 3977 | 4203 | <?php if ($page > 1): ?> |
| 3978 | 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> |
| 3979 | 4205 | <?php endif; ?> |
| @@ -3986,10 +4212,10 @@ | ||
| 3986 | 4212 | <?php if ($page < $total_pages): ?> |
| 3987 | 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> |
| 3988 | 4214 | <?php endif; ?> |
| 3989 | 4215 | </div> |
| 3990 | - </div> | |
| 3991 | - <?php endif; ?> | |
| 4216 | + <?php endif; ?> | |
| 4217 | + </div> | |
| 3992 | 4218 | </div> |
| 3993 | 4219 | <?php $this->render_layout_close(); ?> |
| 3994 | 4220 | |
| 3995 | 4221 | <script> |
| @@ -4070,8 +4296,19 @@ | ||
| 4070 | 4296 | if (!empty($value)) { |
| 4071 | 4297 | $query_parts[] = $key . '=' . urlencode($value); |
| 4072 | 4298 | } |
| 4073 | 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 | + | |
| 4074 | 4311 | return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; |
| 4075 | 4312 | } |
| 4076 | 4313 | |
| 4077 | 4314 | /** |
| @@ -4330,8 +4567,9 @@ | ||
| 4330 | 4567 | <!-- Per-Core Load Threshold --> |
| 4331 | 4568 | <div style="margin-bottom: 24px;"> |
| 4332 | 4569 | <label for="cpu_load_per_core_threshold" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 4333 | 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.'); ?> | |
| 4334 | 4572 | </label> |
| 4335 | 4573 | <input type="number" |
| 4336 | 4574 | id="cpu_load_per_core_threshold" |
| 4337 | 4575 | name="metasync_options[performance][cpu_load_per_core_threshold]" |
| @@ -4883,88 +5121,8 @@ | ||
| 4883 | 5121 | |
| 4884 | 5122 | /** |
| 4885 | 5123 | * Get the settings option array and print one of its values |
| 4886 | 5124 | */ |
| 4887 | - public function no_index_posts_callback() | |
| 4888 | - { | |
| 4889 | - Metasync_Settings_Fields::instance()->no_index_posts_callback(); | |
| 4890 | - } | |
| 4891 | - | |
| 4892 | - /** | |
| 4893 | - * Get the settings option array and print one of its values | |
| 4894 | - */ | |
| 4895 | - public function no_follow_links_callback() | |
| 4896 | - { | |
| 4897 | - Metasync_Settings_Fields::instance()->no_follow_links_callback(); | |
| 4898 | - } | |
| 4899 | - | |
| 4900 | - /** | |
| 4901 | - * Get the settings option array and print one of its values | |
| 4902 | - */ | |
| 4903 | - public function open_external_links_callback() | |
| 4904 | - { | |
| 4905 | - Metasync_Settings_Fields::instance()->open_external_links_callback(); | |
| 4906 | - } | |
| 4907 | - | |
| 4908 | - /** | |
| 4909 | - * Get the settings option array and print one of its values | |
| 4910 | - */ | |
| 4911 | - public function add_alt_image_tags_callback() | |
| 4912 | - { | |
| 4913 | - Metasync_Settings_Fields::instance()->add_alt_image_tags_callback(); | |
| 4914 | - } | |
| 4915 | - | |
| 4916 | - /** | |
| 4917 | - * Get the settings option array and print one of its values | |
| 4918 | - */ | |
| 4919 | - public function add_title_image_tags_callback() | |
| 4920 | - { | |
| 4921 | - Metasync_Settings_Fields::instance()->add_title_image_tags_callback(); | |
| 4922 | - } | |
| 4923 | - | |
| 4924 | - /** | |
| 4925 | - * Get the settings option array and print one of its values | |
| 4926 | - */ | |
| 4927 | - public function site_type_callback() | |
| 4928 | - { | |
| 4929 | - Metasync_Settings_Fields::instance()->site_type_callback(); | |
| 4930 | - } | |
| 4931 | - | |
| 4932 | - /** | |
| 4933 | - * Get the settings option array and print one of its values | |
| 4934 | - */ | |
| 4935 | - public function site_business_type_callback() | |
| 4936 | - { | |
| 4937 | - Metasync_Settings_Fields::instance()->site_business_type_callback(); | |
| 4938 | - } | |
| 4939 | - | |
| 4940 | - /** | |
| 4941 | - * Get the settings option array and print one of its values | |
| 4942 | - */ | |
| 4943 | - public function site_company_name_callback() | |
| 4944 | - { | |
| 4945 | - Metasync_Settings_Fields::instance()->site_company_name_callback(); | |
| 4946 | - } | |
| 4947 | - | |
| 4948 | - /** | |
| 4949 | - * Get the settings option array and print one of its values | |
| 4950 | - */ | |
| 4951 | - public function site_google_logo_callback() | |
| 4952 | - { | |
| 4953 | - Metasync_Settings_Fields::instance()->site_google_logo_callback(); | |
| 4954 | - } | |
| 4955 | - | |
| 4956 | - /** | |
| 4957 | - * Get the settings option array and print one of its values | |
| 4958 | - */ | |
| 4959 | - public function site_social_share_image_callback() | |
| 4960 | - { | |
| 4961 | - Metasync_Settings_Fields::instance()->site_social_share_image_callback(); | |
| 4962 | - } | |
| 4963 | - | |
| 4964 | - /** | |
| 4965 | - * Get the settings option array and print one of its values | |
| 4966 | - */ | |
| 4967 | 5125 | public function common_robot_meta_tags_callback() |
| 4968 | 5126 | { |
| 4969 | 5127 | Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback(); |
| 4970 | 5128 | } |
| @@ -5162,8 +5320,14 @@ | ||
| 5162 | 5320 | * Show a one-time admin notice when a page builder is detected but the |
| 5163 | 5321 | * "Default Page Builder" setting has never been explicitly saved. |
| 5164 | 5322 | */ |
| 5165 | 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 | + | |
| 5166 | 5330 | $configured = Metasync::get_option('general')['default_page_builder'] ?? ''; |
| 5167 | 5331 | |
| 5168 | 5332 | // Setting already saved — nothing to warn about |
| 5169 | 5333 | if (!empty($configured)) { |
| @@ -6353,8 +6517,18 @@ | ||
| 6353 | 6517 | if (!isset($_POST['submit'])) { |
| 6354 | 6518 | return; |
| 6355 | 6519 | } |
| 6356 | 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 | + | |
| 6357 | 6531 | // Save post types for Google Instant Indexing auto-submit |
| 6358 | 6532 | if (isset($_POST['metasync_post_types'])) { |
| 6359 | 6533 | $post_data = metasync_sanitize_input_array($_POST); |
| 6360 | 6534 | $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : []; |
| @@ -6438,12 +6612,24 @@ | ||
| 6438 | 6612 | if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') { |
| 6439 | 6613 | $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); |
| 6440 | 6614 | $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; |
| 6441 | 6615 | |
| 6442 | - if (in_array($post->post_type, $post_types) && function_exists('google_index_direct')) { | |
| 6443 | - $service_info = google_index_direct()->get_service_account_info(); | |
| 6444 | - if (!isset($service_info['error'])) { | |
| 6445 | - 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 | + } | |
| 6446 | 6632 | } |
| 6447 | 6633 | } |
| 6448 | 6634 | } |
| 6449 | 6635 | |