PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
← All changes | admin/class-metasync-admin.php +305 -56 2.6.212.7.0 View file →
@@ -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