| @@ -41,9 +41,8 @@ | ||
| 41 | 41 | const SECTION_SEO_CONTROLS = "seo_controls"; |
| 42 | 42 | const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced"; |
| 43 | 43 | const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index"; |
| 44 | 44 | const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings"; |
| 45 | - const SECTION_BREADCRUMBS = "breadcrumbs_settings"; | |
| 46 | 45 | const SECTION_LLMS_TXT = "llms_txt_settings"; |
| 47 | 46 | |
| 48 | 47 | /** |
| 49 | 48 | * The ID of this plugin. |
| @@ -264,8 +263,9 @@ | ||
| 264 | 263 | add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0); |
| 265 | 264 | |
| 266 | 265 | add_action('admin_init', array($this, 'initialize_cookie')); |
| 267 | 266 | add_action('admin_init', array($this, 'maybe_redirect_to_wizard')); |
| 267 | + add_action('admin_init', array($this, 'redirect_legacy_instant_indexing_page')); | |
| 268 | 268 | |
| 269 | 269 | // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed) |
| 270 | 270 | add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins')); |
| 271 | 271 | add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all')); |
| @@ -283,8 +283,11 @@ | ||
| 283 | 283 | |
| 284 | 284 | // Add AJAX for saving execution settings |
| 285 | 285 | add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') ); |
| 286 | 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 | + | |
| 287 | 290 | // Add AJAX for saving hosting cache settings |
| 288 | 291 | add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') ); |
| 289 | 292 | |
| 290 | 293 | // Add AJAX for saving OTTO Cache TTL |
| @@ -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;"> |
| @@ -2708,8 +2824,15 @@ | ||
| 2708 | 2824 | // Handle form submissions |
| 2709 | 2825 | if (isset($_POST['metasync_media_optimization_nonce'])) { |
| 2710 | 2826 | check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce'); |
| 2711 | 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 | + | |
| 2712 | 2835 | // Handle reset to defaults |
| 2713 | 2836 | if (!empty($_POST['metasync_media_reset'])) { |
| 2714 | 2837 | $defaults = Metasync_Media_Settings::get_defaults(); |
| 2715 | 2838 | Metasync_Media_Settings::save_settings($defaults); |
| @@ -2799,8 +2922,15 @@ | ||
| 2799 | 2922 | if (!$attachment_id) { |
| 2800 | 2923 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2801 | 2924 | } |
| 2802 | 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 | + | |
| 2803 | 2933 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; |
| 2804 | 2934 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php'; |
| 2805 | 2935 | $settings = Metasync_Media_Settings::get_settings(); |
| 2806 | 2936 | |
| @@ -2852,8 +2982,14 @@ | ||
| 2852 | 2982 | if (!$attachment_id) { |
| 2853 | 2983 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2854 | 2984 | } |
| 2855 | 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 | + | |
| 2856 | 2992 | // Replace-strategy conversions have no original to restore — reverting |
| 2857 | 2993 | // would delete the attachment's only file. Refuse up front, like bulk revert does. |
| 2858 | 2994 | if (!Metasync_Image_Converter::can_revert($attachment_id)) { |
| 2859 | 2995 | wp_send_json_error(__('This image cannot be reverted. The original file no longer exists (replace strategy).', 'metasync')); |
| @@ -2911,8 +3047,15 @@ | ||
| 2911 | 3047 | public function ajax_batch_progress() |
| 2912 | 3048 | { |
| 2913 | 3049 | check_ajax_referer('metasync_media_opt_nonce', 'nonce'); |
| 2914 | 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 | + | |
| 2915 | 3058 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; |
| 2916 | 3059 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; |
| 2917 | 3060 | |
| 2918 | 3061 | $progress = Metasync_Media_Batch_Optimizer::get_progress(); |
| @@ -2943,10 +3086,19 @@ | ||
| 2943 | 3086 | $settings = Metasync_Media_Settings::get_settings(); |
| 2944 | 3087 | |
| 2945 | 3088 | $success = 0; |
| 2946 | 3089 | $failed = 0; |
| 3090 | + $denied = 0; | |
| 2947 | 3091 | |
| 2948 | 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 | + | |
| 2949 | 3101 | if (Metasync_Image_Converter::convert_attachment($id, $settings)) { |
| 2950 | 3102 | $success++; |
| 2951 | 3103 | } else { |
| 2952 | 3104 | $failed++; |
| @@ -2955,8 +3107,9 @@ | ||
| 2955 | 3107 | |
| 2956 | 3108 | wp_send_json_success([ |
| 2957 | 3109 | 'success' => $success, |
| 2958 | 3110 | 'failed' => $failed, |
| 3111 | + 'denied' => $denied, | |
| 2959 | 3112 | ]); |
| 2960 | 3113 | } |
| 2961 | 3114 | |
| 2962 | 3115 | /** |
| @@ -2979,11 +3132,19 @@ | ||
| 2979 | 3132 | |
| 2980 | 3133 | $success = 0; |
| 2981 | 3134 | $failed = 0; |
| 2982 | 3135 | $skipped = 0; |
| 3136 | + $denied = 0; | |
| 2983 | 3137 | $errors = []; |
| 2984 | 3138 | |
| 2985 | 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 | + | |
| 2986 | 3147 | $format = get_post_meta($id, '_metasync_converted_format', true); |
| 2987 | 3148 | |
| 2988 | 3149 | if (!$format) { |
| 2989 | 3150 | $skipped++; |
| @@ -3011,8 +3172,9 @@ | ||
| 3011 | 3172 | wp_send_json_success([ |
| 3012 | 3173 | 'success' => $success, |
| 3013 | 3174 | 'failed' => $failed, |
| 3014 | 3175 | 'skipped' => $skipped, |
| 3176 | + 'denied' => $denied, | |
| 3015 | 3177 | 'errors' => $errors, |
| 3016 | 3178 | ]); |
| 3017 | 3179 | } |
| 3018 | 3180 | |
| @@ -3088,8 +3250,86 @@ | ||
| 3088 | 3250 | Metasync_Media_Batch_Optimizer::process_batch_tick(); |
| 3089 | 3251 | } |
| 3090 | 3252 | |
| 3091 | 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 | + /** | |
| 3092 | 3332 | * Report Issue page callback |
| 3093 | 3333 | */ |
| 3094 | 3334 | public function create_admin_report_issue_page() |
| 3095 | 3335 | { |
| @@ -3135,8 +3375,10 @@ | ||
| 3135 | 3375 | if (is_wp_error($result)) { |
| 3136 | 3376 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 3137 | 3377 | sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message()) |
| 3138 | 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>'; | |
| 3139 | 3381 | } else { |
| 3140 | 3382 | echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '</p></div>'; |
| 3141 | 3383 | } |
| 3142 | 3384 | } |
| @@ -3199,8 +3441,12 @@ | ||
| 3199 | 3441 | 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])), |
| 3200 | 3442 | 'auto_detect' => isset($_POST['auto_detect']), |
| 3201 | 3443 | 'taxonomies' => $video_taxonomies, |
| 3202 | 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'] ?? '')), | |
| 3203 | 3449 | ]; |
| 3204 | 3450 | |
| 3205 | 3451 | // Always invalidate old cache before saving new settings |
| 3206 | 3452 | delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); |
| @@ -3287,8 +3533,10 @@ | ||
| 3287 | 3533 | error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); |
| 3288 | 3534 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 3289 | 3535 | sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg) |
| 3290 | 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>'; | |
| 3291 | 3539 | } else { |
| 3292 | 3540 | $message = esc_html__('Sitemap generated successfully!', 'metasync'); |
| 3293 | 3541 | if (!empty($extras)) { |
| 3294 | 3542 | $message .= ' ' . sprintf( |
| @@ -3481,47 +3729,8 @@ | ||
| 3481 | 3729 | Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page(); |
| 3482 | 3730 | } |
| 3483 | 3731 | |
| 3484 | 3732 | /** |
| 3485 | - * Google Instant Index Setting page callback | |
| 3486 | - */ | |
| 3487 | - public function create_admin_google_instant_index_page() | |
| 3488 | - { | |
| 3489 | - $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.'); | |
| 3490 | - | |
| 3491 | - // Render shared Google Index credentials section | |
| 3492 | - if (!function_exists('google_index_direct')) { | |
| 3493 | - if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) { | |
| 3494 | - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 3495 | - } else { | |
| 3496 | - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); | |
| 3497 | - return; | |
| 3498 | - } | |
| 3499 | - } | |
| 3500 | - $google_index = google_index_direct(); | |
| 3501 | - $service_info = $google_index->get_service_account_info(); | |
| 3502 | - $is_configured = !isset($service_info['error']); | |
| 3503 | - | |
| 3504 | - $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : ''; | |
| 3505 | - | |
| 3506 | - include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php'; | |
| 3507 | - | |
| 3508 | - // Render post types selection with save form | |
| 3509 | - $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); | |
| 3510 | - $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; | |
| 3511 | - ?> | |
| 3512 | - <form method="POST" action=""> | |
| 3513 | - <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?> | |
| 3514 | - <div class="dashboard-card" style="padding: 20px;"> | |
| 3515 | - <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?> | |
| 3516 | - </div> | |
| 3517 | - </form> | |
| 3518 | - <?php | |
| 3519 | - | |
| 3520 | - $this->render_layout_close(); | |
| 3521 | - } | |
| 3522 | - | |
| 3523 | - /** | |
| 3524 | 3733 | * Google Console page callback |
| 3525 | 3734 | */ |
| 3526 | 3735 | public function create_admin_google_console_page() |
| 3527 | 3736 | { |
| @@ -3821,9 +4030,9 @@ | ||
| 3821 | 4030 | } |
| 3822 | 4031 | |
| 3823 | 4032 | // Get pagination parameters |
| 3824 | 4033 | $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; |
| 3825 | - $per_page = 10; | |
| 4034 | + $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10); | |
| 3826 | 4035 | $offset = ($page - 1) * $per_page; |
| 3827 | 4036 | |
| 3828 | 4037 | // Get filters |
| 3829 | 4038 | $filters = [ |
| @@ -3982,14 +4191,15 @@ | ||
| 3982 | 4191 | <?php endif; ?> |
| 3983 | 4192 | </div> |
| 3984 | 4193 | |
| 3985 | 4194 | <!-- Pagination --> |
| 3986 | - <?php if ($total_pages > 1): ?> | |
| 3987 | - <div class="sync-log-pagination"> | |
| 3988 | - <div class="sync-log-pagination-info"> | |
| 3989 | - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?> | |
| 3990 | - </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> | |
| 3991 | 4200 | |
| 4201 | + <?php if ($total_pages > 1): ?> | |
| 3992 | 4202 | <div class="sync-log-pagination-controls"> |
| 3993 | 4203 | <?php if ($page > 1): ?> |
| 3994 | 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> |
| 3995 | 4205 | <?php endif; ?> |
| @@ -4002,10 +4212,10 @@ | ||
| 4002 | 4212 | <?php if ($page < $total_pages): ?> |
| 4003 | 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> |
| 4004 | 4214 | <?php endif; ?> |
| 4005 | 4215 | </div> |
| 4006 | - </div> | |
| 4007 | - <?php endif; ?> | |
| 4216 | + <?php endif; ?> | |
| 4217 | + </div> | |
| 4008 | 4218 | </div> |
| 4009 | 4219 | <?php $this->render_layout_close(); ?> |
| 4010 | 4220 | |
| 4011 | 4221 | <script> |
| @@ -4086,8 +4296,19 @@ | ||
| 4086 | 4296 | if (!empty($value)) { |
| 4087 | 4297 | $query_parts[] = $key . '=' . urlencode($value); |
| 4088 | 4298 | } |
| 4089 | 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 | + | |
| 4090 | 4311 | return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; |
| 4091 | 4312 | } |
| 4092 | 4313 | |
| 4093 | 4314 | /** |
| @@ -5099,8 +5320,14 @@ | ||
| 5099 | 5320 | * Show a one-time admin notice when a page builder is detected but the |
| 5100 | 5321 | * "Default Page Builder" setting has never been explicitly saved. |
| 5101 | 5322 | */ |
| 5102 | 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 | + | |
| 5103 | 5330 | $configured = Metasync::get_option('general')['default_page_builder'] ?? ''; |
| 5104 | 5331 | |
| 5105 | 5332 | // Setting already saved — nothing to warn about |
| 5106 | 5333 | if (!empty($configured)) { |
| @@ -6290,8 +6517,18 @@ | ||
| 6290 | 6517 | if (!isset($_POST['submit'])) { |
| 6291 | 6518 | return; |
| 6292 | 6519 | } |
| 6293 | 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 | + | |
| 6294 | 6531 | // Save post types for Google Instant Indexing auto-submit |
| 6295 | 6532 | if (isset($_POST['metasync_post_types'])) { |
| 6296 | 6533 | $post_data = metasync_sanitize_input_array($_POST); |
| 6297 | 6534 | $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : []; |
| @@ -6375,12 +6612,24 @@ | ||
| 6375 | 6612 | if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') { |
| 6376 | 6613 | $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); |
| 6377 | 6614 | $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; |
| 6378 | 6615 | |
| 6379 | - if (in_array($post->post_type, $post_types) && function_exists('google_index_direct')) { | |
| 6380 | - $service_info = google_index_direct()->get_service_account_info(); | |
| 6381 | - if (!isset($service_info['error'])) { | |
| 6382 | - 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 | + } | |
| 6383 | 6632 | } |
| 6384 | 6633 | } |
| 6385 | 6634 | } |
| 6386 | 6635 | |