| @@ -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. |
| @@ -198,13 +196,18 @@ | ||
| 198 | 196 | $data = Metasync::get_option('general'); |
| 199 | 197 | |
| 200 | 198 | // Set menu title using the effective title (includes whitelabel company name if available) |
| 201 | 199 | $this->menu_title = $this->get_effective_menu_title(); |
| 202 | - if(!isset( $data['white_label_plugin_menu_slug'])){ | |
| 203 | - self::$page_slug = "searchatlas"; | |
| 204 | - }else{ | |
| 205 | - self::$page_slug = $data['white_label_plugin_menu_slug']=="" ? "searchatlas":$data['white_label_plugin_menu_slug']; | |
| 206 | - } | |
| 200 | + $raw_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : ''; | |
| 201 | + $clean_slug = sanitize_title($raw_slug); | |
| 202 | + # Self-heal a legacy URL-shaped slug: persist the sanitized value so every | |
| 203 | + # reader that builds admin links from the stored option gets a valid WP menu slug. | |
| 204 | + if ($raw_slug !== '' && $clean_slug !== $raw_slug) { | |
| 205 | + $options = Metasync::get_option(); | |
| 206 | + $options['general']['white_label_plugin_menu_slug'] = $clean_slug; | |
| 207 | + Metasync::set_option($options); | |
| 208 | + } | |
| 209 | + self::$page_slug = $clean_slug === '' ? 'searchatlas' : $clean_slug; | |
| 207 | 210 | |
| 208 | 211 | add_action('admin_menu', array($this, 'add_plugin_settings_page')); |
| 209 | 212 | add_action('admin_menu', array($this, 'add_import_external_data_page')); |
| 210 | 213 | add_action('admin_init', array($this, 'settings_page_init')); |
| @@ -281,8 +284,11 @@ | ||
| 281 | 284 | add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') ); |
| 282 | 285 | |
| 283 | 286 | // Add AJAX for saving hosting cache settings |
| 284 | 287 | add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') ); |
| 288 | + | |
| 289 | + // Add AJAX for saving OTTO Cache TTL | |
| 290 | + add_action('wp_ajax_metasync_save_otto_cache_ttl', array($this, 'ajax_save_otto_cache_ttl')); | |
| 285 | 291 | add_action( 'wp_ajax_metasync_save_object_cache_settings', array($this, 'ajax_save_object_cache_settings') ); |
| 286 | 292 | |
| 287 | 293 | // Add AJAX for saving edge cache / CDN settings |
| 288 | 294 | add_action( 'wp_ajax_metasync_save_edge_cache_settings', array('Metasync_Edge_Cache_Settings', 'ajax_save') ); |
| @@ -307,8 +313,9 @@ | ||
| 307 | 313 | add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots')); |
| 308 | 314 | add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup')); |
| 309 | 315 | add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup')); |
| 310 | 316 | add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup')); |
| 317 | + add_action('wp_ajax_metasync_get_robots_backups', array($this, 'ajax_get_robots_backups')); | |
| 311 | 318 | |
| 312 | 319 | // Add AJAX handlers for host blocking test |
| 313 | 320 | add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get')); |
| 314 | 321 | add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post')); |
| @@ -362,8 +369,9 @@ | ||
| 362 | 369 | add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress')); |
| 363 | 370 | add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected')); |
| 364 | 371 | add_action('wp_ajax_metasync_bulk_unoptimize_selected', array($this, 'ajax_bulk_unoptimize_selected')); |
| 365 | 372 | add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick')); |
| 373 | + add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image')); | |
| 366 | 374 | add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron')); |
| 367 | 375 | |
| 368 | 376 | # Add AJAX handlers for Google Instant Indexing |
| 369 | 377 | add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi')); |
| @@ -458,9 +466,11 @@ | ||
| 458 | 466 | |
| 459 | 467 | $data= Metasync::get_option('general'); |
| 460 | 468 | |
| 461 | 469 | # Get white label menu slug |
| 462 | - $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : $data['white_label_plugin_menu_slug']; | |
| 470 | + # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug | |
| 471 | + $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']); | |
| 472 | + $menu_slug = $menu_slug === '' ? self::$page_slug : $menu_slug; | |
| 463 | 473 | |
| 464 | 474 | ?> |
| 465 | 475 | <style> |
| 466 | 476 | #toplevel_page_<?php echo esc_attr(str_replace(' ', '-',$menu_slug)); ?> .wp-menu-image.dashicons-before img { |
| @@ -481,19 +491,125 @@ | ||
| 481 | 491 | if ( ! isset( $_GET['page'] ) || strpos( $_GET['page'], self::$page_slug ) !== 0 ) { |
| 482 | 492 | return; |
| 483 | 493 | } |
| 484 | 494 | ?> |
| 485 | - <style>.metasync-dashboard-wrap { opacity: 0; transition: opacity 0.15s ease-in; }</style> | |
| 495 | + <style> | |
| 496 | + .metasync-dashboard-wrap { opacity: 0; transition: opacity 0.15s ease-in; } | |
| 497 | + /* | |
| 498 | + * WP prints admin notices at the top of #wpbody-content, then core's | |
| 499 | + * common.js relocates them next to .wp-header-end inside our wrap on | |
| 500 | + * DOM-ready. Keep them out of the flow until that move so they don't | |
| 501 | + * flash above the dashboard header and shove the page down. | |
| 502 | + * Once relocated they're no longer a direct child here, so this rule | |
| 503 | + * stops matching and they fade in with the wrap. We mirror core's | |
| 504 | + * relocation set exactly (.notice/.updated/.error, minus .inline and | |
| 505 | + * .below-h2); .update-nag is left alone since core never moves it. | |
| 506 | + */ | |
| 507 | + #wpbody-content > .notice:not(.inline):not(.below-h2), | |
| 508 | + #wpbody-content > .updated:not(.inline):not(.below-h2), | |
| 509 | + #wpbody-content > .error:not(.inline):not(.below-h2) { display: none !important; } | |
| 510 | + </style> | |
| 486 | 511 | <?php |
| 487 | 512 | } |
| 488 | 513 | |
| 489 | 514 | public function suppress_notices_on_wizard_page() { |
| 490 | - if ( isset( $_GET['page'] ) && strpos( $_GET['page'], '-setup-wizard' ) !== false ) { | |
| 515 | + if ( ! isset( $_GET['page'] ) ) { | |
| 516 | + return; | |
| 517 | + } | |
| 518 | + | |
| 519 | + $page = sanitize_text_field( wp_unslash( $_GET['page'] ) ); | |
| 520 | + | |
| 521 | + // Setup wizard: strip every notice for a fully focused, distraction-free screen. | |
| 522 | + if ( strpos( $page, '-setup-wizard' ) !== false ) { | |
| 491 | 523 | remove_all_actions( 'admin_notices' ); |
| 492 | 524 | remove_all_actions( 'all_admin_notices' ); |
| 525 | + return; | |
| 493 | 526 | } |
| 527 | + | |
| 528 | + // Other Search Atlas plugin pages: strip third-party notices but keep our own. | |
| 529 | + if ( strpos( $page, self::$page_slug ) === 0 ) { | |
| 530 | + $this->remove_third_party_admin_notices(); | |
| 531 | + } | |
| 494 | 532 | } |
| 495 | 533 | |
| 534 | + /** | |
| 535 | + * Remove admin-notice callbacks that don't belong to this plugin. | |
| 536 | + * | |
| 537 | + * Third-party plugins (e.g. Rank Math, Yoast) register their banners on the | |
| 538 | + * global admin_notices / all_admin_notices actions, which WordPress fires on | |
| 539 | + * every admin screen — so their notices leak onto our settings pages. We walk | |
| 540 | + * the registered callbacks and drop any whose code lives outside this plugin's | |
| 541 | + * directory, leaving every one of our own notices intact. | |
| 542 | + */ | |
| 543 | + private function remove_third_party_admin_notices() { | |
| 544 | + global $wp_filter; | |
| 545 | + | |
| 546 | + foreach ( array( 'admin_notices', 'all_admin_notices' ) as $hook ) { | |
| 547 | + if ( empty( $wp_filter[ $hook ] ) || ! ( $wp_filter[ $hook ] instanceof WP_Hook ) ) { | |
| 548 | + continue; | |
| 549 | + } | |
| 550 | + | |
| 551 | + // Collect first, then remove — avoids mutating the callbacks array mid-walk. | |
| 552 | + $to_remove = array(); | |
| 553 | + foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) { | |
| 554 | + foreach ( $callbacks as $callback ) { | |
| 555 | + if ( ! $this->is_own_admin_notice_callback( $callback['function'] ) ) { | |
| 556 | + $to_remove[] = array( $callback['function'], $priority ); | |
| 557 | + } | |
| 558 | + } | |
| 559 | + } | |
| 560 | + | |
| 561 | + foreach ( $to_remove as $entry ) { | |
| 562 | + remove_action( $hook, $entry[0], $entry[1] ); | |
| 563 | + } | |
| 564 | + } | |
| 565 | + } | |
| 566 | + | |
| 567 | + /** | |
| 568 | + * Determine whether an admin-notice callback is defined by this plugin. | |
| 569 | + * | |
| 570 | + * Ownership is decided by where the callback's code physically lives, not by | |
| 571 | + * class name — our notice owners use inconsistent prefixes (Metasync_Admin, | |
| 572 | + * Google_Index_Admin, ConfigControllerMetaSync), and a white-label install can | |
| 573 | + * rename the plugin folder. Resolving the declaring file and checking it sits | |
| 574 | + * inside this plugin's directory is robust against all of that. If a callback | |
| 575 | + * can't be introspected, we keep it rather than risk hiding a legitimate notice. | |
| 576 | + * | |
| 577 | + * @param string|array|Closure $function The registered callback. | |
| 578 | + * @return bool True when the callback's code lives inside this plugin. | |
| 579 | + */ | |
| 580 | + private function is_own_admin_notice_callback( $function ) { | |
| 581 | + // Plugin root: this file lives in <plugin>/admin, so its parent is the root. | |
| 582 | + $plugin_dir = wp_normalize_path( dirname( __DIR__ ) ) . '/'; | |
| 583 | + | |
| 584 | + try { | |
| 585 | + if ( is_array( $function ) && isset( $function[0], $function[1] ) ) { | |
| 586 | + $class = is_object( $function[0] ) ? get_class( $function[0] ) : $function[0]; | |
| 587 | + $ref = new ReflectionMethod( $class, $function[1] ); | |
| 588 | + } elseif ( $function instanceof Closure ) { | |
| 589 | + $ref = new ReflectionFunction( $function ); | |
| 590 | + } elseif ( is_string( $function ) && strpos( $function, '::' ) !== false ) { | |
| 591 | + list( $class, $method ) = explode( '::', $function, 2 ); | |
| 592 | + $ref = new ReflectionMethod( $class, $method ); | |
| 593 | + } elseif ( is_string( $function ) ) { | |
| 594 | + $ref = new ReflectionFunction( $function ); | |
| 595 | + } else { | |
| 596 | + return false; | |
| 597 | + } | |
| 598 | + } catch ( \Throwable $e ) { | |
| 599 | + // Unintrospectable callback — keep it; never hide a notice we can't classify. | |
| 600 | + return true; | |
| 601 | + } | |
| 602 | + | |
| 603 | + $file = $ref->getFileName(); | |
| 604 | + if ( ! $file ) { | |
| 605 | + // Internal/built-in callback (no source file) — not ours. | |
| 606 | + return false; | |
| 607 | + } | |
| 608 | + | |
| 609 | + return strpos( wp_normalize_path( $file ), $plugin_dir ) === 0; | |
| 610 | + } | |
| 611 | + | |
| 496 | 612 | #---------fixes issue : #95 ---------- |
| 497 | 613 | #This function is to redirect in case client changes slug on fresh install |
| 498 | 614 | #It is called by the add_option hook |
| 499 | 615 | |
| @@ -853,10 +969,41 @@ | ||
| 853 | 969 | array('jquery'), |
| 854 | 970 | $this->version, |
| 855 | 971 | true |
| 856 | 972 | ); |
| 973 | + | |
| 974 | + // Keeps core's admin-menu pinning in step with pages whose height | |
| 975 | + // changes after load. Depends on 'common' so core has bound its | |
| 976 | + // own handlers before this runs. | |
| 977 | + wp_enqueue_script( | |
| 978 | + $this->plugin_name . '-admin-menu-height', | |
| 979 | + plugin_dir_url(__FILE__) . 'js/metasync-admin-menu-height.js', | |
| 980 | + array('jquery', 'common'), | |
| 981 | + $this->version, | |
| 982 | + true | |
| 983 | + ); | |
| 857 | 984 | } |
| 858 | 985 | |
| 986 | + $per_page_screens = [ | |
| 987 | + self::$page_slug . '-redirections', | |
| 988 | + self::$page_slug . '-404-monitor', | |
| 989 | + self::$page_slug . '-sync-log', | |
| 990 | + self::$page_slug . '-media-optimization', | |
| 991 | + ]; | |
| 992 | + | |
| 993 | + if (in_array($current_page, $per_page_screens, true)) { | |
| 994 | + // Results-per-page selectors use this external handler instead of | |
| 995 | + // an inline event attribute so changing a page size always reloads | |
| 996 | + // the current admin page. | |
| 997 | + wp_enqueue_script( | |
| 998 | + $this->plugin_name . '-per-page', | |
| 999 | + plugin_dir_url(__FILE__) . 'js/metasync-per-page.js', | |
| 1000 | + array(), | |
| 1001 | + $this->version, | |
| 1002 | + true | |
| 1003 | + ); | |
| 1004 | + } | |
| 1005 | + | |
| 859 | 1006 | // Dashboard iframe height (only on dashboard page) |
| 860 | 1007 | if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) { |
| 861 | 1008 | wp_enqueue_script( |
| 862 | 1009 | $this->plugin_name . '-iframe', |
| @@ -1041,9 +1188,8 @@ | ||
| 1041 | 1188 | true |
| 1042 | 1189 | ); |
| 1043 | 1190 | wp_localize_script($this->plugin_name . '-connect', 'metasyncConnectData', array( |
| 1044 | 1191 | 'optionKey' => self::option_key, |
| 1045 | - 'storedPassword' => isset($whitelabel_data['settings_password']) ? $whitelabel_data['settings_password'] : '', | |
| 1046 | 1192 | 'adminPostUrl' => admin_url('admin-post.php'), |
| 1047 | 1193 | 'exportNonce' => wp_create_nonce('metasync_export_whitelabel'), |
| 1048 | 1194 | 'ajaxUrl' => admin_url('admin-ajax.php'), |
| 1049 | 1195 | 'logoutNonceField' => wp_nonce_field('whitelabel_logout_nonce', 'whitelabel_logout_nonce', true, false), |
| @@ -1131,8 +1277,11 @@ | ||
| 1131 | 1277 | array('jquery'), |
| 1132 | 1278 | $this->version, |
| 1133 | 1279 | true |
| 1134 | 1280 | ); |
| 1281 | + wp_localize_script($this->plugin_name . '-bing-console', 'metasyncBingConsoleData', array( | |
| 1282 | + 'nonce' => wp_create_nonce('metasync_nonce'), | |
| 1283 | + )); | |
| 1135 | 1284 | } |
| 1136 | 1285 | |
| 1137 | 1286 | // Add redirection form |
| 1138 | 1287 | if ($current_page === self::$page_slug . '-redirections') { |
| @@ -1327,14 +1476,44 @@ | ||
| 1327 | 1476 | } |
| 1328 | 1477 | |
| 1329 | 1478 | /** |
| 1330 | 1479 | * Settings of HeartBeat API for admin area. |
| 1331 | - * Set time interval of send request. | |
| 1480 | + * | |
| 1481 | + * The only thing the plugin rides the beat for is a periodic admin | |
| 1482 | + * telemetry ping (the `heartbeat-send` handler in admin/js/metasync-admin.js), | |
| 1483 | + * so it slows the beat down rather than speeding it up. Two contexts are | |
| 1484 | + * left on whatever core decided, because core leans on the beat there: | |
| 1485 | + * | |
| 1486 | + * - The front end, where the beat drives wp-auth-check — the "your session | |
| 1487 | + * has expired, log in again" prompt. | |
| 1488 | + * - The editor screens, where each tick renews the post lock. A lock only | |
| 1489 | + * lives for wp_check_post_lock_window() seconds (150 by default), so any | |
| 1490 | + * interval longer than that drops the lock between beats and lets a | |
| 1491 | + * second editor silently take over a post someone still has open. | |
| 1492 | + * | |
| 1493 | + * Core clamps this to 1-3600 seconds in wp-includes/js/heartbeat.js, so an | |
| 1494 | + * over-long interval is not corrected on our behalf. | |
| 1495 | + * | |
| 1496 | + * @param array $settings Heartbeat settings array. | |
| 1497 | + * @return array Heartbeat settings array. | |
| 1332 | 1498 | */ |
| 1333 | 1499 | function metasync_heartbeat_settings($settings) |
| 1334 | 1500 | { |
| 1335 | - global $heartbeat_frequency; | |
| 1336 | - $settings['interval'] = 300; | |
| 1501 | + if (!is_admin()) { | |
| 1502 | + return $settings; | |
| 1503 | + } | |
| 1504 | + | |
| 1505 | + # $pagenow is set in wp-includes/vars.php, long before this filter runs; | |
| 1506 | + # core's own wp_heartbeat_set_suspension() reads it exactly this way. | |
| 1507 | + global $pagenow; | |
| 1508 | + $lock_dependent_screens = array('post.php', 'post-new.php', 'customize.php'); | |
| 1509 | + if (isset($pagenow) && in_array($pagenow, $lock_dependent_screens, true)) { | |
| 1510 | + return $settings; | |
| 1511 | + } | |
| 1512 | + | |
| 1513 | + # 60s matches heartbeat.js's own default and stays well inside the | |
| 1514 | + # 150s post-lock window, so nothing core does can fall through a gap. | |
| 1515 | + $settings['interval'] = 60; | |
| 1337 | 1516 | return $settings; |
| 1338 | 1517 | } |
| 1339 | 1518 | |
| 1340 | 1519 | /** |
| @@ -1503,8 +1682,47 @@ | ||
| 1503 | 1682 | // Get cache count |
| 1504 | 1683 | $cache_count = Metasync_Otto_Transient_Cache::get_cache_count(); |
| 1505 | 1684 | |
| 1506 | 1685 | ?> |
| 1686 | + <!-- OTTO Cache TTL Setting --> | |
| 1687 | + <div style="margin-bottom: 30px; padding-top: 20px;"> | |
| 1688 | + <h4 style="margin-top: 0; color: var(--dashboard-text-primary);"><?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Cache TTL</h4> | |
| 1689 | + <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);"> | |
| 1690 | + 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. | |
| 1691 | + </p> | |
| 1692 | + | |
| 1693 | + <div style="background: rgba(255,255,255,0.02); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> | |
| 1694 | + <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;"> | |
| 1695 | + <label for="metasync-otto-cache-ttl" style="color: var(--dashboard-text-primary); font-weight: 500;"> | |
| 1696 | + Cache TTL (minutes): | |
| 1697 | + </label> | |
| 1698 | + <?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.'); ?> | |
| 1699 | + <input type="number" | |
| 1700 | + id="metasync-otto-cache-ttl" | |
| 1701 | + value="<?php echo esc_attr($this->get_otto_cache_ttl_minutes()); ?>" | |
| 1702 | + min="30" | |
| 1703 | + max="1440" | |
| 1704 | + step="1" | |
| 1705 | + style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg, rgba(255,255,255,0.05)); color: var(--dashboard-text-primary);" /> | |
| 1706 | + <span style="color: var(--dashboard-text-secondary); font-size: 13px;">min 30 · max 1440</span> | |
| 1707 | + </div> | |
| 1708 | + | |
| 1709 | + <div style="display: flex; align-items: center; gap: 12px;"> | |
| 1710 | + <button type="button" | |
| 1711 | + id="metasync-otto-ttl-save-btn" | |
| 1712 | + class="metasync-btn-primary" | |
| 1713 | + style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 9px 18px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;" | |
| 1714 | + onmouseover="this.style.transform='translateY(-1px)';" | |
| 1715 | + onmouseout="this.style.transform='translateY(0)';"> | |
| 1716 | + Save TTL | |
| 1717 | + </button> | |
| 1718 | + <span id="metasync-otto-ttl-save-msg" style="display: none; font-size: 13px;"></span> | |
| 1719 | + </div> | |
| 1720 | + | |
| 1721 | + <input type="hidden" id="metasync-otto-ttl-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_otto_cache_ttl_nonce')); ?>" /> | |
| 1722 | + </div> | |
| 1723 | + </div> | |
| 1724 | + | |
| 1507 | 1725 | <!-- Cache Plugin Management --> |
| 1508 | 1726 | <div style="margin-bottom: 30px;"> |
| 1509 | 1727 | <h4 style="margin-top: 0; color: var(--dashboard-text-primary);">Clear All Cache Plugins</h4> |
| 1510 | 1728 | <p style="margin-bottom: 15px; color: var(--dashboard-text-secondary);">Clear all cache plugins to ensure changes are visible immediately.</p> |
| @@ -1622,8 +1840,9 @@ | ||
| 1622 | 1840 | <?php checked(true, !empty($hosting_settings['wpengine_enabled'])); ?> |
| 1623 | 1841 | <?php echo !$wpe_detected ? 'disabled' : ''; ?> |
| 1624 | 1842 | style="width: 16px; height: 16px; cursor: <?php echo $wpe_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1625 | 1843 | <span style="color: var(--dashboard-text-primary); font-weight: 500;">WP Engine</span> |
| 1844 | + <?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.'); ?> | |
| 1626 | 1845 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges Varnish + Memcached</span> |
| 1627 | 1846 | </label> |
| 1628 | 1847 | |
| 1629 | 1848 | <label style="display: flex; align-items: center; gap: 10px; margin-bottom: 16px; cursor: pointer;"> |
| @@ -1632,8 +1851,9 @@ | ||
| 1632 | 1851 | <?php checked(true, !empty($hosting_settings['kinsta_enabled'])); ?> |
| 1633 | 1852 | <?php echo !$kinsta_detected ? 'disabled' : ''; ?> |
| 1634 | 1853 | style="width: 16px; height: 16px; cursor: <?php echo $kinsta_detected ? 'pointer' : 'not-allowed'; ?>;" /> |
| 1635 | 1854 | <span style="color: var(--dashboard-text-primary); font-weight: 500;">Kinsta</span> |
| 1855 | + <?php Metasync::render_tooltip_icon('hosting_cache_kinsta', 'If hosted on Kinsta, turn on so OTTO clears Kinsta\'s server cache automatically.'); ?> | |
| 1636 | 1856 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;">— purges full-page cache (kinsta_cache_purge_full)</span> |
| 1637 | 1857 | </label> |
| 1638 | 1858 | |
| 1639 | 1859 | <div style="display: flex; align-items: center; gap: 12px;"> |
| @@ -1647,9 +1867,9 @@ | ||
| 1647 | 1867 | </button> |
| 1648 | 1868 | <span id="metasync-hc-save-msg" style="display: none; font-size: 13px;"></span> |
| 1649 | 1869 | </div> |
| 1650 | 1870 | |
| 1651 | - <input type="hidden" id="metasync-hc-nonce" value="<?php echo wp_create_nonce('metasync_hosting_cache_nonce'); ?>" /> | |
| 1871 | + <input type="hidden" id="metasync-hc-nonce" value="<?php echo esc_attr(wp_create_nonce('metasync_hosting_cache_nonce')); ?>" /> | |
| 1652 | 1872 | </div> |
| 1653 | 1873 | |
| 1654 | 1874 | <!-- Purge button --> |
| 1655 | 1875 | <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> |
| @@ -1724,8 +1944,46 @@ | ||
| 1724 | 1944 | setTimeout(function() { $msg.fadeOut(); }, 4000); |
| 1725 | 1945 | } |
| 1726 | 1946 | }); |
| 1727 | 1947 | }); |
| 1948 | + | |
| 1949 | + // OTTO Cache TTL save | |
| 1950 | + $('#metasync-otto-ttl-save-btn').on('click', function() { | |
| 1951 | + var $btn = $(this); | |
| 1952 | + var $msg = $('#metasync-otto-ttl-save-msg'); | |
| 1953 | + var ttl = parseInt($('#metasync-otto-cache-ttl').val(), 10); | |
| 1954 | + | |
| 1955 | + if (isNaN(ttl) || ttl < 30 || ttl > 1440) { | |
| 1956 | + $msg.text('❌ TTL must be between 30 and 1440 minutes.').css('color', '#ef4444').show(); | |
| 1957 | + return; | |
| 1958 | + } | |
| 1959 | + | |
| 1960 | + $btn.prop('disabled', true).text('Saving…'); | |
| 1961 | + | |
| 1962 | + $.ajax({ | |
| 1963 | + url: ajaxurl, | |
| 1964 | + type: 'POST', | |
| 1965 | + data: { | |
| 1966 | + action: 'metasync_save_otto_cache_ttl', | |
| 1967 | + otto_cache_ttl_nonce: $('#metasync-otto-ttl-nonce').val(), | |
| 1968 | + otto_cache_ttl: ttl, | |
| 1969 | + }, | |
| 1970 | + success: function(response) { | |
| 1971 | + if (response.success) { | |
| 1972 | + $msg.text('✅ Saved').css('color', '#22c55e').show(); | |
| 1973 | + } else { | |
| 1974 | + $msg.text('❌ ' + (response.data && response.data.message ? response.data.message : 'Save failed')).css('color', '#ef4444').show(); | |
| 1975 | + } | |
| 1976 | + }, | |
| 1977 | + error: function() { | |
| 1978 | + $msg.text('❌ Request failed').css('color', '#ef4444').show(); | |
| 1979 | + }, | |
| 1980 | + complete: function() { | |
| 1981 | + $btn.prop('disabled', false).text('Save TTL'); | |
| 1982 | + setTimeout(function() { $msg.fadeOut(); }, 4000); | |
| 1983 | + } | |
| 1984 | + }); | |
| 1985 | + }); | |
| 1728 | 1986 | }); |
| 1729 | 1987 | </script> |
| 1730 | 1988 | </div> |
| 1731 | 1989 | |
| @@ -1743,9 +2001,9 @@ | ||
| 1743 | 2001 | id="metasync-targeted-object-cache" |
| 1744 | 2002 | <?php checked('1', $targeted_cache_enabled); ?> |
| 1745 | 2003 | style="width: 16px; height: 16px; cursor: pointer; margin-top: 2px; flex-shrink: 0;" /> |
| 1746 | 2004 | <span> |
| 1747 | - <span style="color: var(--dashboard-text-primary); font-weight: 500; display: block; margin-bottom: 4px;">Targeted Object Cache Purge</span> | |
| 2005 | + <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> | |
| 1748 | 2006 | <span style="color: var(--dashboard-text-secondary); font-size: 12px;"> |
| 1749 | 2007 | When enabled, only the updated posts are evicted from the object cache (recommended for large sites). |
| 1750 | 2008 | When disabled, a full <code>wp_cache_flush()</code> is used instead. |
| 1751 | 2009 | </span> |
| @@ -1983,8 +2241,46 @@ | ||
| 1983 | 2241 | wp_send_json_success(array('message' => 'Object cache settings saved')); |
| 1984 | 2242 | } |
| 1985 | 2243 | |
| 1986 | 2244 | /** |
| 2245 | + * Get OTTO Cache TTL value in minutes from execution settings. | |
| 2246 | + * | |
| 2247 | + * @return int | |
| 2248 | + */ | |
| 2249 | + private function get_otto_cache_ttl_minutes() { | |
| 2250 | + $execution_settings = get_option('metasync_execution_settings', array()); | |
| 2251 | + return isset($execution_settings['otto_cache_ttl']) ? absint($execution_settings['otto_cache_ttl']) : 30; | |
| 2252 | + } | |
| 2253 | + | |
| 2254 | + /** | |
| 2255 | + * AJAX handler for saving OTTO Cache TTL | |
| 2256 | + */ | |
| 2257 | + public function ajax_save_otto_cache_ttl() { | |
| 2258 | + if (!isset($_POST['otto_cache_ttl_nonce']) || !wp_verify_nonce($_POST['otto_cache_ttl_nonce'], 'metasync_otto_cache_ttl_nonce')) { | |
| 2259 | + wp_send_json_error(array('message' => 'Security check failed.')); | |
| 2260 | + return; | |
| 2261 | + } | |
| 2262 | + | |
| 2263 | + if (!current_user_can('manage_options')) { | |
| 2264 | + wp_send_json_error(array('message' => 'Insufficient permissions.')); | |
| 2265 | + return; | |
| 2266 | + } | |
| 2267 | + | |
| 2268 | + $ttl = isset($_POST['otto_cache_ttl']) ? absint($_POST['otto_cache_ttl']) : 30; | |
| 2269 | + | |
| 2270 | + if ($ttl < 30 || $ttl > 1440) { | |
| 2271 | + wp_send_json_error(array('message' => sprintf('%s Cache TTL must be between 30 and 1440 minutes.', Metasync::get_whitelabel_otto_name()))); | |
| 2272 | + return; | |
| 2273 | + } | |
| 2274 | + | |
| 2275 | + $settings = get_option('metasync_execution_settings', array()); | |
| 2276 | + $settings['otto_cache_ttl'] = $ttl; | |
| 2277 | + update_option('metasync_execution_settings', $settings); | |
| 2278 | + | |
| 2279 | + wp_send_json_success(array('message' => sprintf('%s Cache TTL saved.', Metasync::get_whitelabel_otto_name()))); | |
| 2280 | + } | |
| 2281 | + | |
| 2282 | + /** | |
| 1987 | 2283 | * admin_post handler: purge WP Engine and Kinsta hosting-level caches |
| 1988 | 2284 | */ |
| 1989 | 2285 | public function handle_purge_hosting_cache() { |
| 1990 | 2286 | if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce($_POST['hosting_cache_purge_nonce'], 'metasync_hosting_cache_purge_nonce')) { |
| @@ -2472,8 +2768,15 @@ | ||
| 2472 | 2768 | // Handle form submissions |
| 2473 | 2769 | if (isset($_POST['metasync_media_optimization_nonce'])) { |
| 2474 | 2770 | check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce'); |
| 2475 | 2771 | |
| 2772 | + // Settings shape the whole site's conversion pipeline — restrict | |
| 2773 | + // the save (and the reset) to site admins, not just anyone who can | |
| 2774 | + // open the page via plugin_access_roles. | |
| 2775 | + if (!current_user_can('manage_options')) { | |
| 2776 | + wp_die(__('Sorry, you are not allowed to manage media optimization settings.', 'metasync')); | |
| 2777 | + } | |
| 2778 | + | |
| 2476 | 2779 | // Handle reset to defaults |
| 2477 | 2780 | if (!empty($_POST['metasync_media_reset'])) { |
| 2478 | 2781 | $defaults = Metasync_Media_Settings::get_defaults(); |
| 2479 | 2782 | Metasync_Media_Settings::save_settings($defaults); |
| @@ -2563,8 +2866,15 @@ | ||
| 2563 | 2866 | if (!$attachment_id) { |
| 2564 | 2867 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2565 | 2868 | } |
| 2566 | 2869 | |
| 2870 | + // Ownership: under the replace strategy this permanently deletes the | |
| 2871 | + // attachment's original file, so upload_files alone is not enough — | |
| 2872 | + // the caller must be allowed to edit this specific attachment. | |
| 2873 | + if (!current_user_can('edit_post', $attachment_id)) { | |
| 2874 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 2875 | + } | |
| 2876 | + | |
| 2567 | 2877 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; |
| 2568 | 2878 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php'; |
| 2569 | 2879 | $settings = Metasync_Media_Settings::get_settings(); |
| 2570 | 2880 | |
| @@ -2616,8 +2926,20 @@ | ||
| 2616 | 2926 | if (!$attachment_id) { |
| 2617 | 2927 | wp_send_json_error(__('Invalid attachment ID.', 'metasync')); |
| 2618 | 2928 | } |
| 2619 | 2929 | |
| 2930 | + // Ownership: reverting deletes the attachment's converted files, so | |
| 2931 | + // restrict it to users who may edit this specific attachment. | |
| 2932 | + if (!current_user_can('edit_post', $attachment_id)) { | |
| 2933 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 2934 | + } | |
| 2935 | + | |
| 2936 | + // Replace-strategy conversions have no original to restore — reverting | |
| 2937 | + // would delete the attachment's only file. Refuse up front, like bulk revert does. | |
| 2938 | + if (!Metasync_Image_Converter::can_revert($attachment_id)) { | |
| 2939 | + wp_send_json_error(__('This image cannot be reverted. The original file no longer exists (replace strategy).', 'metasync')); | |
| 2940 | + } | |
| 2941 | + | |
| 2620 | 2942 | $success = Metasync_Image_Converter::revert_attachment($attachment_id); |
| 2621 | 2943 | |
| 2622 | 2944 | if ($success) { |
| 2623 | 2945 | wp_send_json_success(); |
| @@ -2669,8 +2991,15 @@ | ||
| 2669 | 2991 | public function ajax_batch_progress() |
| 2670 | 2992 | { |
| 2671 | 2993 | check_ajax_referer('metasync_media_opt_nonce', 'nonce'); |
| 2672 | 2994 | |
| 2995 | + // Read-only progress/stats polling — the same capability that can | |
| 2996 | + // open the media optimization page is enough, but a nonce alone | |
| 2997 | + // never was. | |
| 2998 | + if (!current_user_can('upload_files')) { | |
| 2999 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3000 | + } | |
| 3001 | + | |
| 2673 | 3002 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; |
| 2674 | 3003 | require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; |
| 2675 | 3004 | |
| 2676 | 3005 | $progress = Metasync_Media_Batch_Optimizer::get_progress(); |
| @@ -2701,10 +3030,19 @@ | ||
| 2701 | 3030 | $settings = Metasync_Media_Settings::get_settings(); |
| 2702 | 3031 | |
| 2703 | 3032 | $success = 0; |
| 2704 | 3033 | $failed = 0; |
| 3034 | + $denied = 0; | |
| 2705 | 3035 | |
| 2706 | 3036 | foreach ($ids as $id) { |
| 3037 | + // Per-attachment ownership: replace mode deletes originals, so | |
| 3038 | + // upload_files alone must not allow converting images the caller | |
| 3039 | + // does not own/cannot edit. | |
| 3040 | + if (!current_user_can('edit_post', $id)) { | |
| 3041 | + $denied++; | |
| 3042 | + continue; | |
| 3043 | + } | |
| 3044 | + | |
| 2707 | 3045 | if (Metasync_Image_Converter::convert_attachment($id, $settings)) { |
| 2708 | 3046 | $success++; |
| 2709 | 3047 | } else { |
| 2710 | 3048 | $failed++; |
| @@ -2713,8 +3051,9 @@ | ||
| 2713 | 3051 | |
| 2714 | 3052 | wp_send_json_success([ |
| 2715 | 3053 | 'success' => $success, |
| 2716 | 3054 | 'failed' => $failed, |
| 3055 | + 'denied' => $denied, | |
| 2717 | 3056 | ]); |
| 2718 | 3057 | } |
| 2719 | 3058 | |
| 2720 | 3059 | /** |
| @@ -2737,11 +3076,19 @@ | ||
| 2737 | 3076 | |
| 2738 | 3077 | $success = 0; |
| 2739 | 3078 | $failed = 0; |
| 2740 | 3079 | $skipped = 0; |
| 3080 | + $denied = 0; | |
| 2741 | 3081 | $errors = []; |
| 2742 | 3082 | |
| 2743 | 3083 | foreach ($ids as $id) { |
| 3084 | + // Per-attachment ownership: reverting deletes the attachment's | |
| 3085 | + // converted files, so restrict it per image, not per role. | |
| 3086 | + if (!current_user_can('edit_post', $id)) { | |
| 3087 | + $denied++; | |
| 3088 | + continue; | |
| 3089 | + } | |
| 3090 | + | |
| 2744 | 3091 | $format = get_post_meta($id, '_metasync_converted_format', true); |
| 2745 | 3092 | |
| 2746 | 3093 | if (!$format) { |
| 2747 | 3094 | $skipped++; |
| @@ -2769,8 +3116,9 @@ | ||
| 2769 | 3116 | wp_send_json_success([ |
| 2770 | 3117 | 'success' => $success, |
| 2771 | 3118 | 'failed' => $failed, |
| 2772 | 3119 | 'skipped' => $skipped, |
| 3120 | + 'denied' => $denied, | |
| 2773 | 3121 | 'errors' => $errors, |
| 2774 | 3122 | ]); |
| 2775 | 3123 | } |
| 2776 | 3124 | |
| @@ -2794,8 +3142,49 @@ | ||
| 2794 | 3142 | wp_send_json_success($progress); |
| 2795 | 3143 | } |
| 2796 | 3144 | |
| 2797 | 3145 | /** |
| 3146 | + * AJAX: Delete an orphaned media record (attachment whose file is missing). | |
| 3147 | + * | |
| 3148 | + * Only deletes when the file is confirmed missing on disk, so valid | |
| 3149 | + * attachments can never be removed through this endpoint. | |
| 3150 | + */ | |
| 3151 | + public function ajax_delete_orphaned_image() | |
| 3152 | + { | |
| 3153 | + check_ajax_referer('metasync_media_opt_nonce', 'nonce'); | |
| 3154 | + | |
| 3155 | + $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; | |
| 3156 | + if (!$attachment_id) { | |
| 3157 | + wp_send_json_error(__('Invalid attachment ID.', 'metasync')); | |
| 3158 | + } | |
| 3159 | + | |
| 3160 | + if (!current_user_can('delete_post', $attachment_id)) { | |
| 3161 | + wp_send_json_error(__('Permission denied.', 'metasync')); | |
| 3162 | + } | |
| 3163 | + | |
| 3164 | + if (get_post_type($attachment_id) !== 'attachment') { | |
| 3165 | + wp_send_json_error(__('Not an attachment.', 'metasync')); | |
| 3166 | + } | |
| 3167 | + | |
| 3168 | + require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; | |
| 3169 | + | |
| 3170 | + // Guard: only orphaned records (missing file) may be deleted here. | |
| 3171 | + if (!Metasync_Media_Library_List_Table::is_file_missing($attachment_id)) { | |
| 3172 | + wp_send_json_error(__('The image file exists; refusing to delete a valid attachment.', 'metasync')); | |
| 3173 | + } | |
| 3174 | + | |
| 3175 | + $deleted = wp_delete_attachment($attachment_id, true); | |
| 3176 | + | |
| 3177 | + if ($deleted) { | |
| 3178 | + wp_send_json_success([ | |
| 3179 | + 'stats' => Metasync_Media_Library_List_Table::get_stats(), | |
| 3180 | + ]); | |
| 3181 | + } | |
| 3182 | + | |
| 3183 | + wp_send_json_error(__('Failed to delete the orphaned media record.', 'metasync')); | |
| 3184 | + } | |
| 3185 | + | |
| 3186 | + /** | |
| 2798 | 3187 | * Cron handler: Process batch optimization tick. |
| 2799 | 3188 | */ |
| 2800 | 3189 | public function handle_media_batch_cron() |
| 2801 | 3190 | { |
| @@ -2852,8 +3241,10 @@ | ||
| 2852 | 3241 | if (is_wp_error($result)) { |
| 2853 | 3242 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 2854 | 3243 | sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message()) |
| 2855 | 3244 | ) . '</p></div>'; |
| 3245 | + } elseif (false === $result) { | |
| 3246 | + 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>'; | |
| 2856 | 3247 | } else { |
| 2857 | 3248 | echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '</p></div>'; |
| 2858 | 3249 | } |
| 2859 | 3250 | } |
| @@ -2916,8 +3307,12 @@ | ||
| 2916 | 3307 | 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])), |
| 2917 | 3308 | 'auto_detect' => isset($_POST['auto_detect']), |
| 2918 | 3309 | 'taxonomies' => $video_taxonomies, |
| 2919 | 3310 | 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['video_excluded_urls'] ?? '')), |
| 3311 | + // This array replaces the stored option wholesale, so every | |
| 3312 | + // setting must be represented here or it is wiped on save. | |
| 3313 | + 'video_url_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_url_meta_keys'] ?? '')), | |
| 3314 | + 'video_thumbnail_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_thumbnail_meta_keys'] ?? '')), | |
| 2920 | 3315 | ]; |
| 2921 | 3316 | |
| 2922 | 3317 | // Always invalidate old cache before saving new settings |
| 2923 | 3318 | delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); |
| @@ -2975,8 +3370,17 @@ | ||
| 2975 | 3370 | if ($sitemap_generator->generate_news_sitemap()) { |
| 2976 | 3371 | $extras[] = 'news'; |
| 2977 | 3372 | } |
| 2978 | 3373 | } |
| 3374 | + | |
| 3375 | + // Reclaim unreferenced memory between the news and video passes | |
| 3376 | + // so the video generator starts with more headroom (they share | |
| 3377 | + // one request). Each generator already releases its own object | |
| 3378 | + // cache via clean_post_cache(), so no cache flush is needed here. | |
| 3379 | + if (function_exists('gc_collect_cycles')) { | |
| 3380 | + gc_collect_cycles(); | |
| 3381 | + } | |
| 3382 | + | |
| 2979 | 3383 | if (!empty($video_opts['enabled'])) { |
| 2980 | 3384 | if ($sitemap_generator->generate_video_sitemap()) { |
| 2981 | 3385 | $extras[] = 'video'; |
| 2982 | 3386 | } |
| @@ -2981,8 +3385,13 @@ | ||
| 2981 | 3385 | $extras[] = 'video'; |
| 2982 | 3386 | } |
| 2983 | 3387 | } |
| 2984 | 3388 | |
| 3389 | + // Same inter-pass GC before the main sitemap pass begins. | |
| 3390 | + if (function_exists('gc_collect_cycles')) { | |
| 3391 | + gc_collect_cycles(); | |
| 3392 | + } | |
| 3393 | + | |
| 2985 | 3394 | // Generate main sitemap (its index will include news/video since they now exist) |
| 2986 | 3395 | $result = $sitemap_generator->generate_sitemap(); |
| 2987 | 3396 | |
| 2988 | 3397 | if (is_wp_error($result)) { |
| @@ -2990,8 +3399,10 @@ | ||
| 2990 | 3399 | error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); |
| 2991 | 3400 | echo '<div class="notice notice-error"><p>' . esc_html( |
| 2992 | 3401 | sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg) |
| 2993 | 3402 | ) . '</p></div>'; |
| 3403 | + } elseif (false === $result) { | |
| 3404 | + 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>'; | |
| 2994 | 3405 | } else { |
| 2995 | 3406 | $message = esc_html__('Sitemap generated successfully!', 'metasync'); |
| 2996 | 3407 | if (!empty($extras)) { |
| 2997 | 3408 | $message .= ' ' . sprintf( |
| @@ -3030,8 +3441,12 @@ | ||
| 3030 | 3441 | |
| 3031 | 3442 | if ($deleted) { |
| 3032 | 3443 | // Disable auto-update only when the general sitemap is removed |
| 3033 | 3444 | update_option('metasync_sitemap_auto_update', false); |
| 3445 | + // Re-enable WP core sitemap only if no other MetaSync sitemaps remain | |
| 3446 | + if (!$sitemap_generator->sitemap_exists()) { | |
| 3447 | + delete_option('metasync_disable_wp_sitemap'); | |
| 3448 | + } | |
| 3034 | 3449 | echo '<div class="notice notice-success"><p>' . esc_html__('General sitemap deleted successfully!', 'metasync') . '</p></div>'; |
| 3035 | 3450 | } else { |
| 3036 | 3451 | echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete general sitemap. The files may not exist or are not writable.', 'metasync') . '</p></div>'; |
| 3037 | 3452 | } |
| @@ -3057,8 +3472,10 @@ | ||
| 3057 | 3472 | |
| 3058 | 3473 | if ($deleted) { |
| 3059 | 3474 | // Also disable auto-update when deleting |
| 3060 | 3475 | update_option('metasync_sitemap_auto_update', false); |
| 3476 | + // Re-enable WP core sitemap so the site isn't left with zero sitemaps | |
| 3477 | + delete_option('metasync_disable_wp_sitemap'); | |
| 3061 | 3478 | echo '<div class="notice notice-success"><p>' . esc_html__('All sitemaps deleted successfully!', 'metasync') . '</p></div>'; |
| 3062 | 3479 | } else { |
| 3063 | 3480 | echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemaps. The files may not exist or are not writable.', 'metasync') . '</p></div>'; |
| 3064 | 3481 | } |
| @@ -3206,8 +3623,9 @@ | ||
| 3206 | 3623 | $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); |
| 3207 | 3624 | $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; |
| 3208 | 3625 | ?> |
| 3209 | 3626 | <form method="POST" action=""> |
| 3627 | + <?php wp_nonce_field('metasync_instant_indexing_settings', 'metasync_instant_indexing_nonce'); ?> | |
| 3210 | 3628 | <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?> |
| 3211 | 3629 | <div class="dashboard-card" style="padding: 20px;"> |
| 3212 | 3630 | <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?> |
| 3213 | 3631 | </div> |
| @@ -3241,16 +3659,8 @@ | ||
| 3241 | 3659 | $this->render_layout_close(); |
| 3242 | 3660 | } |
| 3243 | 3661 | |
| 3244 | 3662 | /** |
| 3245 | - * General Options page callback | |
| 3246 | - */ | |
| 3247 | - public function create_admin_optimal_settings_page() | |
| 3248 | - { | |
| 3249 | - Metasync_Admin_Pages::get_instance($this)->create_admin_optimal_settings_page(); | |
| 3250 | - } | |
| 3251 | - | |
| 3252 | - /** | |
| 3253 | 3663 | * Global Options page callback |
| 3254 | 3664 | */ |
| 3255 | 3665 | public function create_admin_global_settings_page() |
| 3256 | 3666 | { |
| @@ -3282,16 +3692,8 @@ | ||
| 3282 | 3692 | Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page(); |
| 3283 | 3693 | } |
| 3284 | 3694 | |
| 3285 | 3695 | /** |
| 3286 | - * Site Optimal Settings page callback | |
| 3287 | - */ | |
| 3288 | - public function optimization_settings_options() | |
| 3289 | - { | |
| 3290 | - Metasync_Admin_Pages::get_instance($this)->optimization_settings_options(); | |
| 3291 | - } | |
| 3292 | - | |
| 3293 | - /** | |
| 3294 | 3696 | * redirection page callback with tabs |
| 3295 | 3697 | */ |
| 3296 | 3698 | public function create_admin_redirections_page() |
| 3297 | 3699 | { |
| @@ -3318,10 +3720,11 @@ | ||
| 3318 | 3720 | } |
| 3319 | 3721 | delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT ); |
| 3320 | 3722 | echo '<div class="notice notice-warning is-dismissible"><p>'; |
| 3321 | 3723 | printf( |
| 3322 | - /* translators: 1: current load, 2: threshold, 3: core count */ | |
| 3323 | - esc_html__( 'MetaSync: Batch processing was deferred — server CPU load (%1$s) exceeded the threshold (%2$s on %3$s cores). Processing will resume automatically.', 'metasync' ), | |
| 3724 | + /* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */ | |
| 3725 | + esc_html__( '%1$s: Batch processing was deferred — server CPU load (%2$s) exceeded the threshold (%3$s on %4$s cores). Processing will resume automatically.', 'metasync' ), | |
| 3726 | + esc_html( Metasync::get_effective_plugin_name() ), | |
| 3324 | 3727 | esc_html( $data['load'] ), |
| 3325 | 3728 | esc_html( $data['threshold'] ), |
| 3326 | 3729 | esc_html( $data['cores'] ) |
| 3327 | 3730 | ); |
| @@ -3339,9 +3742,9 @@ | ||
| 3339 | 3742 | if (!get_transient('metasync_llms_conflict')) { |
| 3340 | 3743 | return; |
| 3341 | 3744 | } |
| 3342 | 3745 | echo '<div class="notice notice-info is-dismissible"><p>'; |
| 3343 | - echo esc_html__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. MetaSync\'s version takes priority when enabled.', 'metasync'); | |
| 3746 | + echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name())); | |
| 3344 | 3747 | echo '</p></div>'; |
| 3345 | 3748 | } |
| 3346 | 3749 | |
| 3347 | 3750 | /** |
| @@ -3410,8 +3813,17 @@ | ||
| 3410 | 3813 | public function ajax_restore_robots_backup() |
| 3411 | 3814 | { |
| 3412 | 3815 | Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup(); |
| 3413 | 3816 | } |
| 3817 | + | |
| 3818 | + /** | |
| 3819 | + * AJAX handler to fetch a paginated page of robots.txt backups | |
| 3820 | + */ | |
| 3821 | + public function ajax_get_robots_backups() | |
| 3822 | + { | |
| 3823 | + Metasync_Admin_Ajax::instance()->ajax_get_robots_backups(); | |
| 3824 | + } | |
| 3825 | + | |
| 3414 | 3826 | public function ajax_create_redirect_from_404() |
| 3415 | 3827 | { |
| 3416 | 3828 | Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404(); |
| 3417 | 3829 | } |
| @@ -3524,9 +3936,9 @@ | ||
| 3524 | 3936 | } |
| 3525 | 3937 | |
| 3526 | 3938 | // Get pagination parameters |
| 3527 | 3939 | $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; |
| 3528 | - $per_page = 10; | |
| 3940 | + $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10); | |
| 3529 | 3941 | $offset = ($page - 1) * $per_page; |
| 3530 | 3942 | |
| 3531 | 3943 | // Get filters |
| 3532 | 3944 | $filters = [ |
| @@ -3604,9 +4016,9 @@ | ||
| 3604 | 4016 | style="background:#dc3545;color:#fff;border:none;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:13px;" |
| 3605 | 4017 | data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_clear_sync_log')); ?>"> |
| 3606 | 4018 | 🗑 Clear Log |
| 3607 | 4019 | </button> |
| 3608 | - <form method="get" class="sync-filters-form" onchange="this.submit()"> | |
| 4020 | + <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;"> | |
| 3609 | 4021 | <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>"> |
| 3610 | 4022 | |
| 3611 | 4023 | <select name="date_range" class="sync-filter-select"> |
| 3612 | 4024 | <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option> |
| @@ -3656,19 +4068,20 @@ | ||
| 3656 | 4068 | </div> |
| 3657 | 4069 | </div> |
| 3658 | 4070 | |
| 3659 | 4071 | <div class="sync-log-status" style="display:flex;align-items:center;gap:8px;"> |
| 3660 | - <?php if ($record->status === 'published' || $record->status === 'publish'): ?> | |
| 3661 | - <span class="sync-status-badge sync-status-published"> | |
| 3662 | - <span class="sync-status-icon">✓</span> | |
| 3663 | - Published | |
| 3664 | - </span> | |
| 3665 | - <?php else: ?> | |
| 3666 | - <span class="sync-status-badge sync-status-draft"> | |
| 3667 | - <span class="sync-status-icon">i</span> | |
| 3668 | - Draft | |
| 3669 | - </span> | |
| 3670 | - <?php endif; ?> | |
| 4072 | + <?php | |
| 4073 | + $st = (string) $record->status; | |
| 4074 | + $b_label = ucfirst($st); $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; | |
| 4075 | + if ($st === 'published' || $st === 'publish' || $st === 'success' || $st === 'partial') { $b_label = 'Published'; $b_bg = '#16a34a'; $b_icon = 'dashicons-yes'; } | |
| 4076 | + elseif ($st === 'updated') { $b_label = 'Updated'; $b_bg = '#0d9488'; $b_icon = 'dashicons-update'; } | |
| 4077 | + elseif ($st === 'failed' || $st === 'conflict' || $st === 'locked') { $b_label = 'Not imported'; $b_bg = '#64748b'; $b_icon = 'dashicons-minus'; } | |
| 4078 | + elseif ($st === 'draft') { $b_label = 'Draft'; $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; } | |
| 4079 | + ?> | |
| 4080 | + <span class="sync-status-badge" style="display:inline-flex;align-items:center;gap:4px;background:<?php echo esc_attr($b_bg); ?>;color:#fff;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600;line-height:1;"> | |
| 4081 | + <span class="dashicons <?php echo esc_attr($b_icon); ?>" style="font-size:14px;width:14px;height:14px;line-height:14px;"></span> | |
| 4082 | + <?php echo esc_html($b_label); ?> | |
| 4083 | + </span> | |
| 3671 | 4084 | <?php if ($record->source === 'MCP Client'): ?> |
| 3672 | 4085 | <button type="button" |
| 3673 | 4086 | class="metasync-rollback-btn" |
| 3674 | 4087 | data-id="<?php echo esc_attr($record->id); ?>" |
| @@ -3684,14 +4097,15 @@ | ||
| 3684 | 4097 | <?php endif; ?> |
| 3685 | 4098 | </div> |
| 3686 | 4099 | |
| 3687 | 4100 | <!-- Pagination --> |
| 3688 | - <?php if ($total_pages > 1): ?> | |
| 3689 | - <div class="sync-log-pagination"> | |
| 3690 | - <div class="sync-log-pagination-info"> | |
| 3691 | - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?> | |
| 3692 | - </div> | |
| 4101 | + <div class="sync-log-pagination"> | |
| 4102 | + <div class="sync-log-pagination-info"> | |
| 4103 | + 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; ?> | |
| 4104 | + <?php echo Metasync_Per_Page_Helper::render_selector('sync_log', $per_page); ?> | |
| 4105 | + </div> | |
| 3693 | 4106 | |
| 4107 | + <?php if ($total_pages > 1): ?> | |
| 3694 | 4108 | <div class="sync-log-pagination-controls"> |
| 3695 | 4109 | <?php if ($page > 1): ?> |
| 3696 | 4110 | <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> |
| 3697 | 4111 | <?php endif; ?> |
| @@ -3704,10 +4118,10 @@ | ||
| 3704 | 4118 | <?php if ($page < $total_pages): ?> |
| 3705 | 4119 | <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> |
| 3706 | 4120 | <?php endif; ?> |
| 3707 | 4121 | </div> |
| 3708 | - </div> | |
| 3709 | - <?php endif; ?> | |
| 4122 | + <?php endif; ?> | |
| 4123 | + </div> | |
| 3710 | 4124 | </div> |
| 3711 | 4125 | <?php $this->render_layout_close(); ?> |
| 3712 | 4126 | |
| 3713 | 4127 | <script> |
| @@ -3788,8 +4202,19 @@ | ||
| 3788 | 4202 | if (!empty($value)) { |
| 3789 | 4203 | $query_parts[] = $key . '=' . urlencode($value); |
| 3790 | 4204 | } |
| 3791 | 4205 | } |
| 4206 | + | |
| 4207 | + // Preserve the user-selected results-per-page value across page | |
| 4208 | + // navigation so a non-default page size survives clicking a page link. | |
| 4209 | + $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log'); | |
| 4210 | + if (isset($_GET[$per_page_key])) { | |
| 4211 | + $per_page = (int) $_GET[$per_page_key]; | |
| 4212 | + if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) { | |
| 4213 | + $query_parts[] = $per_page_key . '=' . $per_page; | |
| 4214 | + } | |
| 4215 | + } | |
| 4216 | + | |
| 3792 | 4217 | return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; |
| 3793 | 4218 | } |
| 3794 | 4219 | |
| 3795 | 4220 | /** |
| @@ -4020,8 +4445,9 @@ | ||
| 4020 | 4445 | $stats = Metasync_CPU_Monitor::get_stats(); |
| 4021 | 4446 | $per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold(); |
| 4022 | 4447 | $cores = Metasync_CPU_Monitor::get_cpu_core_count(); |
| 4023 | 4448 | $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold(); |
| 4449 | + $detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable(); | |
| 4024 | 4450 | ?> |
| 4025 | 4451 | <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;"> |
| 4026 | 4452 | <!-- CPU Cores Detected --> |
| 4027 | 4453 | <div style="margin-bottom: 24px;"> |
| @@ -4028,12 +4454,20 @@ | ||
| 4028 | 4454 | <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 4029 | 4455 | CPU Cores Detected |
| 4030 | 4456 | </label> |
| 4031 | 4457 | <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);"> |
| 4032 | - <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?> | |
| 4458 | + <?php if ($detection_reliable) : ?> | |
| 4459 | + <strong><?php echo intval($cores); ?></strong> core<?php echo $cores !== 1 ? 's' : ''; ?> | |
| 4460 | + <?php else : ?> | |
| 4461 | + <strong>Not detected</strong> | |
| 4462 | + <?php endif; ?> | |
| 4033 | 4463 | </div> |
| 4034 | 4464 | <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);"> |
| 4035 | - Automatically detected on this system. | |
| 4465 | + <?php if ($detection_reliable) : ?> | |
| 4466 | + Automatically detected on this system. | |
| 4467 | + <?php else : ?> | |
| 4468 | + Core detection is not available on this hosting environment. | |
| 4469 | + <?php endif; ?> | |
| 4036 | 4470 | </p> |
| 4037 | 4471 | </div> |
| 4038 | 4472 | |
| 4039 | 4473 | <!-- Per-Core Load Threshold --> |
| @@ -4039,8 +4473,9 @@ | ||
| 4039 | 4473 | <!-- Per-Core Load Threshold --> |
| 4040 | 4474 | <div style="margin-bottom: 24px;"> |
| 4041 | 4475 | <label for="cpu_load_per_core_threshold" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 4042 | 4476 | Per-Core Load Threshold |
| 4477 | + <?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.'); ?> | |
| 4043 | 4478 | </label> |
| 4044 | 4479 | <input type="number" |
| 4045 | 4480 | id="cpu_load_per_core_threshold" |
| 4046 | 4481 | name="metasync_options[performance][cpu_load_per_core_threshold]" |
| @@ -4592,88 +5027,8 @@ | ||
| 4592 | 5027 | |
| 4593 | 5028 | /** |
| 4594 | 5029 | * Get the settings option array and print one of its values |
| 4595 | 5030 | */ |
| 4596 | - public function no_index_posts_callback() | |
| 4597 | - { | |
| 4598 | - Metasync_Settings_Fields::instance()->no_index_posts_callback(); | |
| 4599 | - } | |
| 4600 | - | |
| 4601 | - /** | |
| 4602 | - * Get the settings option array and print one of its values | |
| 4603 | - */ | |
| 4604 | - public function no_follow_links_callback() | |
| 4605 | - { | |
| 4606 | - Metasync_Settings_Fields::instance()->no_follow_links_callback(); | |
| 4607 | - } | |
| 4608 | - | |
| 4609 | - /** | |
| 4610 | - * Get the settings option array and print one of its values | |
| 4611 | - */ | |
| 4612 | - public function open_external_links_callback() | |
| 4613 | - { | |
| 4614 | - Metasync_Settings_Fields::instance()->open_external_links_callback(); | |
| 4615 | - } | |
| 4616 | - | |
| 4617 | - /** | |
| 4618 | - * Get the settings option array and print one of its values | |
| 4619 | - */ | |
| 4620 | - public function add_alt_image_tags_callback() | |
| 4621 | - { | |
| 4622 | - Metasync_Settings_Fields::instance()->add_alt_image_tags_callback(); | |
| 4623 | - } | |
| 4624 | - | |
| 4625 | - /** | |
| 4626 | - * Get the settings option array and print one of its values | |
| 4627 | - */ | |
| 4628 | - public function add_title_image_tags_callback() | |
| 4629 | - { | |
| 4630 | - Metasync_Settings_Fields::instance()->add_title_image_tags_callback(); | |
| 4631 | - } | |
| 4632 | - | |
| 4633 | - /** | |
| 4634 | - * Get the settings option array and print one of its values | |
| 4635 | - */ | |
| 4636 | - public function site_type_callback() | |
| 4637 | - { | |
| 4638 | - Metasync_Settings_Fields::instance()->site_type_callback(); | |
| 4639 | - } | |
| 4640 | - | |
| 4641 | - /** | |
| 4642 | - * Get the settings option array and print one of its values | |
| 4643 | - */ | |
| 4644 | - public function site_business_type_callback() | |
| 4645 | - { | |
| 4646 | - Metasync_Settings_Fields::instance()->site_business_type_callback(); | |
| 4647 | - } | |
| 4648 | - | |
| 4649 | - /** | |
| 4650 | - * Get the settings option array and print one of its values | |
| 4651 | - */ | |
| 4652 | - public function site_company_name_callback() | |
| 4653 | - { | |
| 4654 | - Metasync_Settings_Fields::instance()->site_company_name_callback(); | |
| 4655 | - } | |
| 4656 | - | |
| 4657 | - /** | |
| 4658 | - * Get the settings option array and print one of its values | |
| 4659 | - */ | |
| 4660 | - public function site_google_logo_callback() | |
| 4661 | - { | |
| 4662 | - Metasync_Settings_Fields::instance()->site_google_logo_callback(); | |
| 4663 | - } | |
| 4664 | - | |
| 4665 | - /** | |
| 4666 | - * Get the settings option array and print one of its values | |
| 4667 | - */ | |
| 4668 | - public function site_social_share_image_callback() | |
| 4669 | - { | |
| 4670 | - Metasync_Settings_Fields::instance()->site_social_share_image_callback(); | |
| 4671 | - } | |
| 4672 | - | |
| 4673 | - /** | |
| 4674 | - * Get the settings option array and print one of its values | |
| 4675 | - */ | |
| 4676 | 5031 | public function common_robot_meta_tags_callback() |
| 4677 | 5032 | { |
| 4678 | 5033 | Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback(); |
| 4679 | 5034 | } |
| @@ -4871,8 +5226,14 @@ | ||
| 4871 | 5226 | * Show a one-time admin notice when a page builder is detected but the |
| 4872 | 5227 | * "Default Page Builder" setting has never been explicitly saved. |
| 4873 | 5228 | */ |
| 4874 | 5229 | public function display_page_builder_notice() { |
| 5230 | + // Only relevant on the plugin's own settings page — avoid repeating | |
| 5231 | + // this notice across every admin screen. | |
| 5232 | + if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) { | |
| 5233 | + return; | |
| 5234 | + } | |
| 5235 | + | |
| 4875 | 5236 | $configured = Metasync::get_option('general')['default_page_builder'] ?? ''; |
| 4876 | 5237 | |
| 4877 | 5238 | // Setting already saved — nothing to warn about |
| 4878 | 5239 | if (!empty($configured)) { |
| @@ -5840,8 +6201,16 @@ | ||
| 5840 | 6201 | if (!$post || !in_array($post->post_type, array('post', 'page'))) { |
| 5841 | 6202 | return; |
| 5842 | 6203 | } |
| 5843 | 6204 | |
| 6205 | + // Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML | |
| 6206 | + // pages: those are raw-HTML store-and-serve pages, NOT actually converted to a | |
| 6207 | + // page builder, so the banner mislabels them. It still shows for pages genuinely | |
| 6208 | + // produced by the converter. | |
| 6209 | + if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) { | |
| 6210 | + return; | |
| 6211 | + } | |
| 6212 | + | |
| 5844 | 6213 | $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true); |
| 5845 | 6214 | $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true); |
| 5846 | 6215 | |
| 5847 | 6216 | if ($has_raw_html || !empty($has_custom_css)) { |
| @@ -5957,8 +6326,14 @@ | ||
| 5957 | 6326 | * @return void Sends JSON response and exits |
| 5958 | 6327 | */ |
| 5959 | 6328 | public function ajax_send_giapi() |
| 5960 | 6329 | { |
| 6330 | + check_ajax_referer('metasync_nonce', 'nonce'); | |
| 6331 | + | |
| 6332 | + if (!Metasync::current_user_has_plugin_access()) { | |
| 6333 | + wp_send_json_error(['message' => 'Insufficient permissions.'], 403); | |
| 6334 | + } | |
| 6335 | + | |
| 5961 | 6336 | $post_data = metasync_sanitize_input_array($_POST); |
| 5962 | 6337 | if (!isset($post_data['metasync_giapi_url'])) { |
| 5963 | 6338 | return; |
| 5964 | 6339 | } |
| @@ -6024,8 +6399,14 @@ | ||
| 6024 | 6399 | * @return void Sends JSON response and exits |
| 6025 | 6400 | */ |
| 6026 | 6401 | public function ajax_send_bing_indexnow() |
| 6027 | 6402 | { |
| 6403 | + check_ajax_referer('metasync_nonce', 'nonce'); | |
| 6404 | + | |
| 6405 | + if (!Metasync::current_user_has_plugin_access()) { | |
| 6406 | + wp_send_json_error(['message' => 'Insufficient permissions.'], 403); | |
| 6407 | + } | |
| 6408 | + | |
| 6028 | 6409 | require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php'; |
| 6029 | 6410 | $bing_instant_index = new Metasync_Bing_Instant_Index(); |
| 6030 | 6411 | $bing_instant_index->send(); |
| 6031 | 6412 | } |
| @@ -6042,8 +6423,18 @@ | ||
| 6042 | 6423 | if (!isset($_POST['submit'])) { |
| 6043 | 6424 | return; |
| 6044 | 6425 | } |
| 6045 | 6426 | |
| 6427 | + // This handler runs on admin_init, which also fires inside | |
| 6428 | + // admin-ajax.php before its login gate, so a bare isset() check let | |
| 6429 | + // any request (including logged-out ones) rewrite the auto-submit | |
| 6430 | + // post types. Require the nonce and plugin access before writing. | |
| 6431 | + if (!isset($_POST['metasync_instant_indexing_nonce']) | |
| 6432 | + || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['metasync_instant_indexing_nonce'])), 'metasync_instant_indexing_settings') | |
| 6433 | + || !Metasync::current_user_has_plugin_access()) { | |
| 6434 | + return; | |
| 6435 | + } | |
| 6436 | + | |
| 6046 | 6437 | // Save post types for Google Instant Indexing auto-submit |
| 6047 | 6438 | if (isset($_POST['metasync_post_types'])) { |
| 6048 | 6439 | $post_data = metasync_sanitize_input_array($_POST); |
| 6049 | 6440 | $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : []; |
| @@ -6127,12 +6518,24 @@ | ||
| 6127 | 6518 | if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') { |
| 6128 | 6519 | $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); |
| 6129 | 6520 | $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; |
| 6130 | 6521 | |
| 6131 | - if (in_array($post->post_type, $post_types) && function_exists('google_index_direct')) { | |
| 6132 | - $service_info = google_index_direct()->get_service_account_info(); | |
| 6133 | - if (!isset($service_info['error'])) { | |
| 6134 | - google_index_direct()->index_post($post_id, $post->post_type, 'update'); | |
| 6522 | + if (in_array($post->post_type, $post_types)) { | |
| 6523 | + // save_post is the single owner of auto-submit and also fires | |
| 6524 | + // on REST-created posts, where nothing else may have loaded | |
| 6525 | + // the Google Index helpers yet — load them on demand instead | |
| 6526 | + // of silently skipping. | |
| 6527 | + if (!function_exists('google_index_direct')) { | |
| 6528 | + $google_index_path = plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 6529 | + if (file_exists($google_index_path)) { | |
| 6530 | + require_once $google_index_path; | |
| 6531 | + } | |
| 6532 | + } | |
| 6533 | + if (function_exists('google_index_direct')) { | |
| 6534 | + $service_info = google_index_direct()->get_service_account_info(); | |
| 6535 | + if (!isset($service_info['error'])) { | |
| 6536 | + google_index_direct()->index_post($post_id, $post->post_type, 'update'); | |
| 6537 | + } | |
| 6135 | 6538 | } |
| 6136 | 6539 | } |
| 6137 | 6540 | } |
| 6138 | 6541 | |