| @@ -46,9 +46,23 @@ | ||
| 46 | 46 | // Accordion / Section helpers |
| 47 | 47 | // ──────────────────────────────────────────────────────────────── |
| 48 | 48 | |
| 49 | 49 | public function get_accordion_sections_config() { |
| 50 | - return array( | |
| 50 | + // Consent switch for writing into other SEO plugins. Loaded here rather | |
| 51 | + // than through the committed classmap so a stale autoload map can never | |
| 52 | + // make the switch — and with it the user's only way to stop those | |
| 53 | + // writes — silently disappear from the page. | |
| 54 | + require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-metasync-seo-sync-settings.php'; | |
| 55 | + | |
| 56 | + $sections = array( | |
| 57 | + 'analytics' => array( | |
| 58 | + 'title' => 'Usage Data & Product Improvement', | |
| 59 | + 'description' => 'Choose whether to share limited usage data to help improve MetaSync', | |
| 60 | + 'icon' => 'chart-bar', | |
| 61 | + 'priority' => 70, | |
| 62 | + 'default_open' => false, | |
| 63 | + 'render_callback' => array($this, 'render_analytics_section'), | |
| 64 | + ), | |
| 51 | 65 | 'connection' => array( |
| 52 | 66 | 'title' => 'Connection & Authentication', |
| 53 | 67 | 'description' => 'Manage your API connection and authentication settings', |
| 54 | 68 | 'icon' => 'admin-network', |
| @@ -68,9 +82,9 @@ | ||
| 68 | 82 | 'fields' => array( |
| 69 | 83 | 'otto_pixel_uuid', |
| 70 | 84 | 'otto_disable_on_loggedin', |
| 71 | 85 | 'otto_disable_preview_button', |
| 72 | - 'otto_wp_rocket_compat', | |
| 86 | + 'seo_priority', | |
| 73 | 87 | ) |
| 74 | 88 | ), |
| 75 | 89 | 'edge_cache' => array( |
| 76 | 90 | 'title' => 'Edge Cache / CDN', |
| @@ -79,8 +93,16 @@ | ||
| 79 | 93 | 'priority' => 22, |
| 80 | 94 | 'default_open' => false, |
| 81 | 95 | 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'), |
| 82 | 96 | ), |
| 97 | + 'seo_plugin_sync' => array( | |
| 98 | + 'title' => 'Sync to Other SEO Plugins', | |
| 99 | + 'description' => 'Control whether ' . Metasync::get_whitelabel_otto_name() . '\'s SEO values are written into Yoast, Rank Math or All in One SEO', | |
| 100 | + 'icon' => 'update', | |
| 101 | + 'priority' => 23, | |
| 102 | + 'default_open' => false, | |
| 103 | + 'render_callback' => array('Metasync_Seo_Sync_Settings', 'render'), | |
| 104 | + ), | |
| 83 | 105 | 'bot_detection' => array( |
| 84 | 106 | 'title' => 'Bot Detection & Filtering', |
| 85 | 107 | 'description' => 'Manage bot traffic and reduce unnecessary API calls', |
| 86 | 108 | 'icon' => 'filter', |
| @@ -92,16 +114,8 @@ | ||
| 92 | 114 | 'otto_bot_blacklist', |
| 93 | 115 | 'otto_bot_statistics_link' |
| 94 | 116 | ) |
| 95 | 117 | ), |
| 96 | - 'breadcrumbs' => array( | |
| 97 | - 'title' => 'Breadcrumbs', | |
| 98 | - 'description' => 'Configure breadcrumb navigation display', | |
| 99 | - 'icon' => 'format-links', | |
| 100 | - 'priority' => 35, | |
| 101 | - 'default_open' => false, | |
| 102 | - 'render_callback' => array($this, 'render_breadcrumbs_section') | |
| 103 | - ), | |
| 104 | 118 | 'open_graph' => array( |
| 105 | 119 | 'title' => 'Open Graph', |
| 106 | 120 | 'description' => 'Control which Open Graph and article meta tags are output', |
| 107 | 121 | 'icon' => 'share', |
| @@ -128,8 +142,10 @@ | ||
| 128 | 142 | 'disable_redirection_metabox', |
| 129 | 143 | 'disable_canonical_metabox', |
| 130 | 144 | 'disable_social_opengraph_metabox', |
| 131 | 145 | 'disable_schema_markup_metabox', |
| 146 | + 'disable_language_alternates_metabox', | |
| 147 | + 'disable_seo_metabox', | |
| 132 | 148 | 'open_external_links' |
| 133 | 149 | ) |
| 134 | 150 | ), |
| 135 | 151 | 'user_management' => array( |
| @@ -169,13 +185,22 @@ | ||
| 169 | 185 | 'fields' => array( |
| 170 | 186 | 'permalink_structure', |
| 171 | 187 | 'hide_dashboard_framework', |
| 172 | 188 | 'show_admin_bar_status', |
| 173 | - 'enable_auto_updates', | |
| 174 | 189 | 'import_external_data' |
| 175 | 190 | ) |
| 176 | 191 | ) |
| 177 | 192 | ); |
| 193 | + | |
| 194 | + // Hidden only when there is nothing to say: no SEO plugin installed, the | |
| 195 | + // switch off, and no saved originals. It is never hidden by the | |
| 196 | + // access-control or whitelabel flags that can conceal Advanced Settings | |
| 197 | + // — a switch that rewrites another plugin's data must stay reachable. | |
| 198 | + if (!Metasync_Seo_Sync_Settings::should_display()) { | |
| 199 | + unset($sections['seo_plugin_sync']); | |
| 200 | + } | |
| 201 | + | |
| 202 | + return $sections; | |
| 178 | 203 | } |
| 179 | 204 | |
| 180 | 205 | public function get_advanced_accordion_config() { |
| 181 | 206 | $config = array(); |
| @@ -200,8 +225,17 @@ | ||
| 200 | 225 | 'default_open' => false, |
| 201 | 226 | 'render_callback' => array($this->admin_instance, 'render_debug_mode_section') |
| 202 | 227 | ); |
| 203 | 228 | |
| 229 | + $config['headless_mode'] = array( | |
| 230 | + 'title' => 'Headless Mode', | |
| 231 | + 'description' => 'Connect WordPress content to a separate public frontend', | |
| 232 | + 'icon' => 'admin-site-alt3', | |
| 233 | + 'priority' => 29, | |
| 234 | + 'default_open' => false, | |
| 235 | + 'render_callback' => array(Metasync_Settings_Registration::instance(), 'render_headless_mode_section') | |
| 236 | + ); | |
| 237 | + | |
| 204 | 238 | $config['error_logs'] = array( |
| 205 | 239 | 'title' => 'Error Logs', |
| 206 | 240 | 'description' => 'View and manage error logs to troubleshoot issues', |
| 207 | 241 | 'icon' => 'warning', |
| @@ -274,9 +308,9 @@ | ||
| 274 | 308 | $content_state = $is_open ? 'open' : 'closed'; |
| 275 | 309 | |
| 276 | 310 | echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">'; |
| 277 | 311 | |
| 278 | - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">'; | |
| 312 | + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">'; | |
| 279 | 313 | echo '<div class="metasync-accordion-title">'; |
| 280 | 314 | echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>'; |
| 281 | 315 | echo '<div class="metasync-accordion-text">'; |
| 282 | 316 | echo '<h3>' . esc_html($section_data['title']) . '</h3>'; |
| @@ -287,9 +321,9 @@ | ||
| 287 | 321 | echo '<span class="toggle-icon">▼</span>'; |
| 288 | 322 | echo '</button>'; |
| 289 | 323 | echo '</div>'; |
| 290 | 324 | |
| 291 | - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">'; | |
| 325 | + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">'; | |
| 292 | 326 | |
| 293 | 327 | if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) { |
| 294 | 328 | call_user_func($section_data['render_callback']); |
| 295 | 329 | } |
| @@ -311,14 +345,15 @@ | ||
| 311 | 345 | </h4> |
| 312 | 346 | <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">This action will permanently delete:</p> |
| 313 | 347 | <ul style="color: var(--dashboard-text-secondary); margin: 0 0 12px 20px; line-height: 1.8;"> |
| 314 | 348 | <li>All API keys and authentication tokens</li> |
| 315 | - <li>White label branding settings</li> | |
| 316 | 349 | <li>Plugin configuration and preferences</li> |
| 317 | 350 | <li>Instant indexing settings</li> |
| 351 | + <li>Media optimization settings and cache</li> | |
| 352 | + <li>Code minification settings</li> | |
| 318 | 353 | <li>All cached data and crawl information</li> |
| 319 | 354 | </ul> |
| 320 | - <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p> | |
| 355 | + <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin's operational settings after this reset.</p> | |
| 321 | 356 | </div> |
| 322 | 357 | <form method="post" action="" onsubmit="return confirmClearSettings(event)"> |
| 323 | 358 | <?php wp_nonce_field('metasync_clear_all_settings_nonce', 'clear_all_settings_nonce'); ?> |
| 324 | 359 | <input type="hidden" name="clear_all_settings" value="yes" /> |
| @@ -326,8 +361,31 @@ | ||
| 326 | 361 | <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings |
| 327 | 362 | </button> |
| 328 | 363 | </form> |
| 329 | 364 | </div> |
| 365 | + <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px; margin-top: 20px;"> | |
| 366 | + <h4 style="color: var(--dashboard-text-primary, #1e1e1e); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;"> | |
| 367 | + <span class="dashicons dashicons-universal-access-alt" style="font-size:18px;width:18px;height:18px;"></span> | |
| 368 | + <span>Delete data on uninstall</span> | |
| 369 | + </h4> | |
| 370 | + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;"> | |
| 371 | + By default, deleting the plugin keeps your data: sitemap and redirection settings, per-post SEO values, and the plugin's database tables stay in place in case you reinstall. Enable this to also remove all of that when the plugin itself is deleted from the Plugins screen. | |
| 372 | + </p> | |
| 373 | + <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;"> | |
| 374 | + The Google service account and the IndexNow API key are always deleted on uninstall, regardless of this setting. | |
| 375 | + </p> | |
| 376 | + <form method="post" action=""> | |
| 377 | + <?php wp_nonce_field('metasync_uninstall_data_toggle_nonce', 'uninstall_data_toggle_nonce'); ?> | |
| 378 | + <input type="hidden" name="metasync_uninstall_data_toggle_form" value="1" /> | |
| 379 | + <label style="color: var(--dashboard-text-primary, #1e1e1e); display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; cursor: pointer;"> | |
| 380 | + <input type="checkbox" name="metasync_delete_data_on_uninstall" value="yes" <?php checked(get_option('metasync_delete_data_on_uninstall', 'no'), 'yes'); ?> /> | |
| 381 | + Delete all plugin data (tables, settings, per-post SEO values, logs) when the plugin is uninstalled | |
| 382 | + </label> | |
| 383 | + <button type="submit" class="button button-secondary"> | |
| 384 | + Save Uninstall Setting | |
| 385 | + </button> | |
| 386 | + </form> | |
| 387 | + </div> | |
| 330 | 388 | <script> |
| 331 | 389 | function confirmClearSettings(event) { |
| 332 | 390 | event.preventDefault(); |
| 333 | 391 | |
| @@ -335,9 +393,9 @@ | ||
| 335 | 393 | if (!firstConfirm) { |
| 336 | 394 | return false; |
| 337 | 395 | } |
| 338 | 396 | |
| 339 | - var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• White label branding settings\n• Plugin configuration and preferences\n• Instant indexing settings\n• All cached data\n\nYou will need to reconfigure the entire plugin from scratch.\n\nType 'DELETE' in the next prompt to confirm."); | |
| 397 | + var secondConfirm = confirm("🚨 FINAL WARNING 🚨\n\nThis will delete:\n• All API keys and authentication tokens\n• Plugin configuration and preferences\n• Instant indexing settings\n• Media optimization settings and cache\n• Code minification settings\n• All cached data\n\nYou will need to reconfigure the plugin's operational settings from scratch.\n\nType 'DELETE' in the next prompt to confirm."); | |
| 340 | 398 | if (!secondConfirm) { |
| 341 | 399 | return false; |
| 342 | 400 | } |
| 343 | 401 | |
| @@ -355,12 +413,21 @@ | ||
| 355 | 413 | } |
| 356 | 414 | |
| 357 | 415 | public function render_google_index_section() { |
| 358 | 416 | if (!function_exists('google_index_direct')) { |
| 359 | - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 417 | + if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) { | |
| 418 | + require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; | |
| 419 | + } else { | |
| 420 | + error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets | |
| 421 | + return; | |
| 422 | + } | |
| 360 | 423 | } |
| 361 | 424 | |
| 362 | 425 | $google_index = google_index_direct(); |
| 426 | + if (!$google_index) { | |
| 427 | + // Nothing to render against, and calling through would be fatal. | |
| 428 | + return; | |
| 429 | + } | |
| 363 | 430 | $service_info = $google_index->get_service_account_info(); |
| 364 | 431 | $is_configured = !isset($service_info['error']); |
| 365 | 432 | |
| 366 | 433 | $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : ''; |
| @@ -437,8 +504,9 @@ | ||
| 437 | 504 | <table class="form-table" style="margin-top: 0;"> |
| 438 | 505 | <tr> |
| 439 | 506 | <th scope="row" style="width: 200px; padding-top: 15px;"> |
| 440 | 507 | <label for="metasync_bing_api_key_inline">IndexNow API Key <span style="color: #d63638;">*</span></label> |
| 508 | + <?php Metasync::render_tooltip_icon('metasync_bing_api_key_inline', 'This key lets Bing trust indexing requests from your site. Click \'Generate Random API Key\' if you don\'t have one — the plugin publishes the matching verification file automatically, no manual upload needed.'); ?> | |
| 441 | 509 | </th> |
| 442 | 510 | <td style="padding-top: 15px;"> |
| 443 | 511 | <input type="text" |
| 444 | 512 | name="metasync_bing_api_key_inline" |
| @@ -638,9 +706,9 @@ | ||
| 638 | 706 | <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;"> |
| 639 | 707 | Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area. |
| 640 | 708 | </p> |
| 641 | 709 | |
| 642 | - <form method="post" action="<?php echo admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced'); ?>" id="plugin-access-roles-form"> | |
| 710 | + <form method="post" action="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced')); ?>" id="plugin-access-roles-form"> | |
| 643 | 711 | <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?> |
| 644 | 712 | <input type="hidden" name="save_plugin_access_roles" value="yes" /> |
| 645 | 713 | |
| 646 | 714 | <div class="metasync-role-selector-container" style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 16px; max-height: 300px; overflow-y: auto;"> |
| @@ -732,8 +800,9 @@ | ||
| 732 | 800 | 'max_memory_limit' => 256, |
| 733 | 801 | 'log_batch_size' => 1000, |
| 734 | 802 | 'action_scheduler_batches' => 1, |
| 735 | 803 | 'otto_rate_limit' => 10, |
| 804 | + 'otto_cache_ttl' => 30, | |
| 736 | 805 | 'queue_cleanup_days' => 31 |
| 737 | 806 | ); |
| 738 | 807 | } |
| 739 | 808 | |
| @@ -757,9 +826,9 @@ | ||
| 757 | 826 | |
| 758 | 827 | public function can_change_memory_limit() { |
| 759 | 828 | $current_limit = ini_get('memory_limit'); |
| 760 | 829 | |
| 761 | - $test_result = @ini_set('memory_limit', $current_limit); | |
| 830 | + $test_result = @ini_set('memory_limit', $current_limit); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API | |
| 762 | 831 | |
| 763 | 832 | return $test_result !== false; |
| 764 | 833 | } |
| 765 | 834 | |
| @@ -765,37 +834,47 @@ | ||
| 765 | 834 | |
| 766 | 835 | public function get_server_limits() { |
| 767 | 836 | $max_execution_time = ini_get('max_execution_time'); |
| 768 | 837 | $memory_limit = ini_get('memory_limit'); |
| 769 | - | |
| 838 | + | |
| 839 | + // Prefer the server-configured value: WordPress (wp_raise_memory_limit) | |
| 840 | + // or this plugin (apply_memory_limit) may have already changed the | |
| 841 | + // runtime limit via ini_set, so ini_get no longer reflects the server | |
| 842 | + // configuration. get_cfg_var reads the php.ini value untouched. | |
| 843 | + $config_memory_limit = get_cfg_var('memory_limit'); | |
| 844 | + if ($config_memory_limit !== false && $config_memory_limit !== '') { | |
| 845 | + $memory_limit = $config_memory_limit; | |
| 846 | + } | |
| 847 | + | |
| 770 | 848 | $memory_limit_mb = $this->parse_memory_limit_to_mb($memory_limit); |
| 771 | - | |
| 772 | - $wp_memory_limit = null; | |
| 773 | - if (defined('WP_MAX_MEMORY_LIMIT')) { | |
| 774 | - $wp_memory_limit = WP_MAX_MEMORY_LIMIT; | |
| 775 | - } elseif (defined('WP_MEMORY_LIMIT')) { | |
| 776 | - $wp_memory_limit = WP_MEMORY_LIMIT; | |
| 777 | - } | |
| 778 | - $wp_memory_limit_mb = $wp_memory_limit ? $this->parse_memory_limit_to_mb($wp_memory_limit) : null; | |
| 779 | - | |
| 780 | - $actual_php_limit_mb = $memory_limit_mb; | |
| 781 | - if ($wp_memory_limit_mb && $wp_memory_limit_mb >= 256) { | |
| 782 | - $actual_php_limit_mb = 128; | |
| 783 | - } | |
| 784 | - | |
| 849 | + | |
| 785 | 850 | $can_change_memory = $this->can_change_memory_limit(); |
| 786 | - | |
| 787 | - $memory_limit_display = $actual_php_limit_mb == -1 ? 'Unlimited' : $actual_php_limit_mb . ' MB'; | |
| 788 | - | |
| 851 | + | |
| 852 | + $memory_limit_display = $memory_limit_mb == -1 ? 'Unlimited' : $memory_limit_mb . ' MB'; | |
| 853 | + | |
| 789 | 854 | return array( |
| 790 | 855 | 'max_execution_time' => $max_execution_time == -1 ? 'Unlimited' : $max_execution_time . ' seconds', |
| 791 | 856 | 'memory_limit' => $memory_limit_display, |
| 792 | 857 | 'max_execution_time_raw' => $max_execution_time, |
| 793 | - 'memory_limit_raw' => $actual_php_limit_mb, | |
| 858 | + 'memory_limit_raw' => $memory_limit_mb, | |
| 794 | 859 | 'can_change_memory' => $can_change_memory |
| 795 | 860 | ); |
| 796 | 861 | } |
| 797 | 862 | |
| 863 | + /** | |
| 864 | + * Allowed range for the Max Memory Limit setting: 64 MB up to the | |
| 865 | + * server-configured memory limit (null max when the server is unlimited). | |
| 866 | + */ | |
| 867 | + public function get_memory_limit_allowed_range() { | |
| 868 | + $server_limits = $this->get_server_limits(); | |
| 869 | + $max = $server_limits['memory_limit_raw'] == -1 ? null : max(64, (int) $server_limits['memory_limit_raw']); | |
| 870 | + | |
| 871 | + return array( | |
| 872 | + 'min' => 64, | |
| 873 | + 'max' => $max | |
| 874 | + ); | |
| 875 | + } | |
| 876 | + | |
| 798 | 877 | public function apply_memory_limit() { |
| 799 | 878 | if (!$this->can_change_memory_limit()) { |
| 800 | 879 | return false; |
| 801 | 880 | } |
| @@ -801,9 +880,9 @@ | ||
| 801 | 880 | } |
| 802 | 881 | |
| 803 | 882 | $memory_limit_mb = $this->get_execution_setting('max_memory_limit'); |
| 804 | 883 | |
| 805 | - $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); | |
| 884 | + $result = @ini_set('memory_limit', $memory_limit_mb . 'M'); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API | |
| 806 | 885 | |
| 807 | 886 | return $result !== false; |
| 808 | 887 | } |
| 809 | 888 | |
| @@ -832,11 +911,12 @@ | ||
| 832 | 911 | |
| 833 | 912 | public function render_execution_settings_section() { |
| 834 | 913 | $settings = $this->get_all_execution_settings(); |
| 835 | 914 | $server_limits = $this->get_server_limits(); |
| 915 | + $memory_range = $this->get_memory_limit_allowed_range(); | |
| 836 | 916 | |
| 837 | 917 | $warnings = array(); |
| 838 | - if ($server_limits['max_execution_time_raw'] != -1 && $settings['max_execution_time'] > $server_limits['max_execution_time_raw']) { | |
| 918 | + if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) { | |
| 839 | 919 | $warnings['max_execution_time'] = true; |
| 840 | 920 | } |
| 841 | 921 | if ($server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) { |
| 842 | 922 | $warnings['max_memory_limit'] = true; |
| @@ -852,16 +932,18 @@ | ||
| 852 | 932 | |
| 853 | 933 | <!-- Execution & Memory Section --> |
| 854 | 934 | <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 855 | 935 | <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Execution & Memory</h3> |
| 856 | - | |
| 857 | - <div style="margin-bottom: 20px;"> | |
| 936 | + | |
| 937 | + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;"> | |
| 938 | + <div> | |
| 858 | 939 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 859 | 940 | Max Execution Time: |
| 941 | + <?php Metasync::render_tooltip_icon('max_execution_time', 'How many seconds a single plugin operation (like a bulk sync or scan) is allowed to run before PHP stops it. Raise this only if you see timeout errors, and only up to what your host allows.'); ?> | |
| 860 | 942 | </label> |
| 861 | 943 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 862 | - <input type="number" | |
| 863 | - id="max_execution_time" | |
| 944 | + <input type="number" | |
| 945 | + id="max_execution_time" | |
| 864 | 946 | name="max_execution_time" |
| 865 | 947 | value="<?php echo esc_attr($settings['max_execution_time']); ?>" |
| 866 | 948 | min="1" |
| 867 | 949 | max="300" |
| @@ -887,16 +969,17 @@ | ||
| 887 | 969 | |
| 888 | 970 | <div> |
| 889 | 971 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 890 | 972 | Max Memory Limit: |
| 973 | + <?php Metasync::render_tooltip_icon('max_memory_limit', 'How much memory (RAM) the plugin is allowed to use for a single operation before PHP stops it. Raise this only if you see "out of memory" errors, and only up to what your host allows.'); ?> | |
| 891 | 974 | </label> |
| 892 | 975 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 893 | - <input type="number" | |
| 894 | - id="max_memory_limit" | |
| 976 | + <input type="number" | |
| 977 | + id="max_memory_limit" | |
| 895 | 978 | name="max_memory_limit" |
| 896 | 979 | value="<?php echo esc_attr($settings['max_memory_limit']); ?>" |
| 897 | - min="64" | |
| 898 | - max="512" | |
| 980 | + min="<?php echo esc_attr($memory_range['min']); ?>" | |
| 981 | + <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?> | |
| 899 | 982 | <?php if (!$server_limits['can_change_memory']): ?> |
| 900 | 983 | readonly |
| 901 | 984 | disabled |
| 902 | 985 | <?php endif; ?> |
| @@ -904,9 +987,9 @@ | ||
| 904 | 987 | style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-card-bg)' : 'rgba(128, 128, 128, 0.1)'; ?>; color: <?php echo $server_limits['can_change_memory'] ? 'var(--dashboard-text-primary)' : 'var(--dashboard-text-secondary)'; ?>; cursor: <?php echo $server_limits['can_change_memory'] ? 'text' : 'not-allowed'; ?>;" /> |
| 905 | 988 | <span style="color: var(--dashboard-text-secondary);">MB</span> |
| 906 | 989 | </div> |
| 907 | 990 | <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;"> |
| 908 | - Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?> | |
| 991 | + Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?> · Allowed: <?php echo $memory_range['max'] !== null ? esc_html($memory_range['min'] . '–' . $memory_range['max'] . ' MB') : esc_html($memory_range['min'] . ' MB or higher'); ?> | |
| 909 | 992 | </p> |
| 910 | 993 | <?php if (!$server_limits['can_change_memory']): ?> |
| 911 | 994 | <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;"> |
| 912 | 995 | <span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span> |
| @@ -925,21 +1008,24 @@ | ||
| 925 | 1008 | </script> |
| 926 | 1009 | <?php endif; ?> |
| 927 | 1010 | <?php endif; ?> |
| 928 | 1011 | </div> |
| 1012 | + </div> | |
| 929 | 1013 | </div> |
| 930 | - | |
| 1014 | + | |
| 931 | 1015 | <!-- Batch Processing Section --> |
| 932 | 1016 | <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 933 | 1017 | <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Batch Processing</h3> |
| 934 | - | |
| 935 | - <div style="margin-bottom: 20px;"> | |
| 1018 | + | |
| 1019 | + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;"> | |
| 1020 | + <div> | |
| 936 | 1021 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 937 | 1022 | Log Processing Batch Size: |
| 1023 | + <?php Metasync::render_tooltip_icon('log_batch_size', 'How many log lines the plugin processes at once when scanning its own logs. Lower it if this causes slowdowns on smaller hosting plans; most sites can leave the default.'); ?> | |
| 938 | 1024 | </label> |
| 939 | 1025 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 940 | - <input type="number" | |
| 941 | - id="log_batch_size" | |
| 1026 | + <input type="number" | |
| 1027 | + id="log_batch_size" | |
| 942 | 1028 | name="log_batch_size" |
| 943 | 1029 | value="<?php echo esc_attr($settings['log_batch_size']); ?>" |
| 944 | 1030 | min="100" |
| 945 | 1031 | max="5000" |
| @@ -951,8 +1037,9 @@ | ||
| 951 | 1037 | |
| 952 | 1038 | <div> |
| 953 | 1039 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 954 | 1040 | Action Scheduler Concurrent Batches: |
| 1041 | + <?php Metasync::render_tooltip_icon('action_scheduler_batches', 'How many background sync jobs the plugin is allowed to run at the same time. Higher values finish syncing faster but use more server resources at once — lower this if your site slows down during syncs.'); ?> | |
| 955 | 1042 | </label> |
| 956 | 1043 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 957 | 1044 | <input type="number" |
| 958 | 1045 | id="action_scheduler_batches" |
| @@ -967,17 +1054,20 @@ | ||
| 967 | 1054 | <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span> |
| 968 | 1055 | <span>Higher values increase server load</span> |
| 969 | 1056 | </p> |
| 970 | 1057 | </div> |
| 1058 | + </div> | |
| 971 | 1059 | </div> |
| 972 | - | |
| 1060 | + | |
| 973 | 1061 | <!-- API Rate Limiting Section --> |
| 974 | 1062 | <div style="background: var(--dashboard-card-bg-alt, rgba(255,255,255,0.05)); border: 1px solid var(--dashboard-border); border-radius: 8px; padding: 20px; margin-bottom: 20px;"> |
| 975 | 1063 | <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">API Rate Limiting</h3> |
| 976 | - | |
| 977 | - <div style="margin-bottom: 20px;"> | |
| 1064 | + | |
| 1065 | + <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;"> | |
| 1066 | + <div> | |
| 978 | 1067 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 979 | - OTTO API Calls Per Minute: | |
| 1068 | + <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API Calls Per Minute: | |
| 1069 | + <?php Metasync::render_tooltip_icon('otto_rate_limit', 'The maximum number of requests per minute the plugin will send to ' . Metasync::get_whitelabel_otto_name() . '\'s servers. Lower it if you\'re seeing rate-limit errors; most sites can leave the default.'); ?> | |
| 980 | 1070 | </label> |
| 981 | 1071 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 982 | 1072 | <input type="number" |
| 983 | 1073 | id="otto_rate_limit" |
| @@ -992,8 +1082,9 @@ | ||
| 992 | 1082 | |
| 993 | 1083 | <div> |
| 994 | 1084 | <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;"> |
| 995 | 1085 | Queue Auto-Cleanup: |
| 1086 | + <?php Metasync::render_tooltip_icon('queue_cleanup_days', 'How many days a completed background job is kept before the plugin automatically deletes its record. Lower it to save database space; raise it if you need to look back further when troubleshooting.'); ?> | |
| 996 | 1087 | </label> |
| 997 | 1088 | <div style="display: flex; align-items: center; gap: 10px;"> |
| 998 | 1089 | <input type="number" |
| 999 | 1090 | id="queue_cleanup_days" |
| @@ -1005,10 +1096,11 @@ | ||
| 1005 | 1096 | style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" /> |
| 1006 | 1097 | <span style="color: var(--dashboard-text-secondary);">days</span> |
| 1007 | 1098 | </div> |
| 1008 | 1099 | </div> |
| 1100 | + </div> | |
| 1009 | 1101 | </div> |
| 1010 | - | |
| 1102 | + | |
| 1011 | 1103 | <!-- Save Button --> |
| 1012 | 1104 | <div style="margin-top: 20px;"> |
| 1013 | 1105 | <button type="submit" |
| 1014 | 1106 | id="metasync-execution-settings-save-btn" |
| @@ -1023,232 +1115,15 @@ | ||
| 1023 | 1115 | <div id="metasync-execution-settings-message" style="display: none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div> |
| 1024 | 1116 | </form> |
| 1025 | 1117 | </div> |
| 1026 | 1118 | |
| 1027 | - <script> | |
| 1028 | - jQuery(document).ready(function($) { | |
| 1029 | - var $form = $('#metasync-execution-settings-form'); | |
| 1030 | - var $saveBtn = $('#metasync-execution-settings-save-btn'); | |
| 1031 | - var $message = $('#metasync-execution-settings-message'); | |
| 1032 | - | |
| 1033 | - var serverMaxExecTime = <?php echo $server_limits['max_execution_time_raw'] == -1 ? 'Infinity' : $server_limits['max_execution_time_raw']; ?>; | |
| 1034 | - var serverMaxMemory = <?php echo $server_limits['memory_limit_raw'] == -1 ? 'Infinity' : $server_limits['memory_limit_raw']; ?>; | |
| 1035 | - var canChangeMemory = <?php echo $server_limits['can_change_memory'] ? 'true' : 'false'; ?>; | |
| 1036 | - | |
| 1037 | - function checkServerLimits() { | |
| 1038 | - var maxExecTime = parseInt($('#max_execution_time').val()) || 0; | |
| 1039 | - var maxMemory = parseInt($('#max_memory_limit').val()) || 0; | |
| 1040 | - | |
| 1041 | - if (serverMaxExecTime !== Infinity && maxExecTime > serverMaxExecTime) { | |
| 1042 | - $('#max_execution_time_warning').show(); | |
| 1043 | - } else { | |
| 1044 | - $('#max_execution_time_warning').hide(); | |
| 1045 | - } | |
| 1046 | - | |
| 1047 | - if (canChangeMemory && serverMaxMemory !== Infinity && maxMemory > serverMaxMemory) { | |
| 1048 | - $('#max_memory_limit_warning').show(); | |
| 1049 | - } else { | |
| 1050 | - $('#max_memory_limit_warning').hide(); | |
| 1051 | - } | |
| 1052 | - } | |
| 1053 | - | |
| 1054 | - $('#max_execution_time, #max_memory_limit').on('input change', function() { | |
| 1055 | - checkServerLimits(); | |
| 1056 | - $(this).css('border-color', 'var(--dashboard-border)'); | |
| 1057 | - }); | |
| 1058 | - | |
| 1059 | - function highlightInvalidField($field, isValid) { | |
| 1060 | - if (isValid) { | |
| 1061 | - $field.css({ | |
| 1062 | - 'border-color': 'var(--dashboard-border)', | |
| 1063 | - 'box-shadow': 'none' | |
| 1064 | - }); | |
| 1065 | - } else { | |
| 1066 | - $field.css({ | |
| 1067 | - 'border-color': '#ef4444', | |
| 1068 | - 'box-shadow': '0 0 0 3px rgba(239, 68, 68, 0.1)' | |
| 1069 | - }); | |
| 1070 | - } | |
| 1071 | - } | |
| 1072 | - | |
| 1073 | - $('#max_execution_time').on('blur', function() { | |
| 1074 | - var value = parseInt($(this).val()) || 0; | |
| 1075 | - var isValid = value >= 1 && value <= 300 && (serverMaxExecTime === Infinity || value <= serverMaxExecTime); | |
| 1076 | - highlightInvalidField($(this), isValid); | |
| 1077 | - }); | |
| 1078 | - | |
| 1079 | - $('#max_memory_limit').on('blur', function() { | |
| 1080 | - if (!canChangeMemory) return; | |
| 1081 | - var value = parseInt($(this).val()) || 0; | |
| 1082 | - var isValid = value >= 64 && value <= 512 && (serverMaxMemory === Infinity || value <= serverMaxMemory); | |
| 1083 | - highlightInvalidField($(this), isValid); | |
| 1084 | - }); | |
| 1085 | - | |
| 1086 | - checkServerLimits(); | |
| 1087 | - | |
| 1088 | - $form.on('submit', function(e) { | |
| 1089 | - e.preventDefault(); | |
| 1090 | - | |
| 1091 | - var formData = { | |
| 1092 | - action: 'metasync_save_execution_settings', | |
| 1093 | - execution_settings_nonce: $('#execution_settings_nonce').val(), | |
| 1094 | - max_execution_time: $('#max_execution_time').val(), | |
| 1095 | - max_memory_limit: $('#max_memory_limit').val(), | |
| 1096 | - log_batch_size: $('#log_batch_size').val(), | |
| 1097 | - action_scheduler_batches: $('#action_scheduler_batches').val(), | |
| 1098 | - otto_rate_limit: $('#otto_rate_limit').val(), | |
| 1099 | - queue_cleanup_days: $('#queue_cleanup_days').val() | |
| 1100 | - }; | |
| 1101 | - | |
| 1102 | - $('input[type="number"]').css({ | |
| 1103 | - 'border-color': 'var(--dashboard-border)', | |
| 1104 | - 'box-shadow': 'none' | |
| 1105 | - }); | |
| 1106 | - | |
| 1107 | - var hasError = false; | |
| 1108 | - var errorField = null; | |
| 1109 | - | |
| 1110 | - if (formData.max_execution_time < 1 || formData.max_execution_time > 300) { | |
| 1111 | - showMessage('Max Execution Time must be between 1 and 300 seconds.', 'error'); | |
| 1112 | - highlightInvalidField($('#max_execution_time'), false); | |
| 1113 | - errorField = $('#max_execution_time'); | |
| 1114 | - hasError = true; | |
| 1115 | - } else if (serverMaxExecTime !== Infinity && formData.max_execution_time > serverMaxExecTime) { | |
| 1116 | - showMessage('Max Execution Time exceeds server limit of ' + serverMaxExecTime + ' seconds. Please reduce the value.', 'error'); | |
| 1117 | - highlightInvalidField($('#max_execution_time'), false); | |
| 1118 | - errorField = $('#max_execution_time'); | |
| 1119 | - hasError = true; | |
| 1120 | - } | |
| 1121 | - | |
| 1122 | - if (canChangeMemory) { | |
| 1123 | - if (formData.max_memory_limit < 64 || formData.max_memory_limit > 512) { | |
| 1124 | - showMessage('Max Memory Limit must be between 64 and 512 MB.', 'error'); | |
| 1125 | - highlightInvalidField($('#max_memory_limit'), false); | |
| 1126 | - if (!hasError) { | |
| 1127 | - errorField = $('#max_memory_limit'); | |
| 1128 | - hasError = true; | |
| 1129 | - } | |
| 1130 | - } else if (serverMaxMemory !== Infinity && formData.max_memory_limit > serverMaxMemory) { | |
| 1131 | - showMessage('Max Memory Limit exceeds server limit of ' + serverMaxMemory + ' MB. Please reduce the value.', 'error'); | |
| 1132 | - highlightInvalidField($('#max_memory_limit'), false); | |
| 1133 | - if (!hasError) { | |
| 1134 | - errorField = $('#max_memory_limit'); | |
| 1135 | - hasError = true; | |
| 1136 | - } | |
| 1137 | - } | |
| 1138 | - } | |
| 1139 | - | |
| 1140 | - if (formData.log_batch_size < 100 || formData.log_batch_size > 5000) { | |
| 1141 | - showMessage('Log Batch Size must be between 100 and 5000 lines.', 'error'); | |
| 1142 | - highlightInvalidField($('#log_batch_size'), false); | |
| 1143 | - if (!hasError) { | |
| 1144 | - errorField = $('#log_batch_size'); | |
| 1145 | - hasError = true; | |
| 1146 | - } | |
| 1147 | - } | |
| 1148 | - if (formData.action_scheduler_batches < 1 || formData.action_scheduler_batches > 10) { | |
| 1149 | - showMessage('Action Scheduler Batches must be between 1 and 10.', 'error'); | |
| 1150 | - highlightInvalidField($('#action_scheduler_batches'), false); | |
| 1151 | - if (!hasError) { | |
| 1152 | - errorField = $('#action_scheduler_batches'); | |
| 1153 | - hasError = true; | |
| 1154 | - } | |
| 1155 | - } | |
| 1156 | - if (formData.otto_rate_limit < 1 || formData.otto_rate_limit > 60) { | |
| 1157 | - showMessage('OTTO Rate Limit must be between 1 and 60 calls per minute.', 'error'); | |
| 1158 | - highlightInvalidField($('#otto_rate_limit'), false); | |
| 1159 | - if (!hasError) { | |
| 1160 | - errorField = $('#otto_rate_limit'); | |
| 1161 | - hasError = true; | |
| 1162 | - } | |
| 1163 | - } | |
| 1164 | - if (formData.queue_cleanup_days < 7 || formData.queue_cleanup_days > 90) { | |
| 1165 | - showMessage('Queue Cleanup Days must be between 7 and 90 days.', 'error'); | |
| 1166 | - highlightInvalidField($('#queue_cleanup_days'), false); | |
| 1167 | - if (!hasError) { | |
| 1168 | - errorField = $('#queue_cleanup_days'); | |
| 1169 | - hasError = true; | |
| 1170 | - } | |
| 1171 | - } | |
| 1172 | - | |
| 1173 | - if (hasError) { | |
| 1174 | - if (errorField) { | |
| 1175 | - errorField.focus(); | |
| 1176 | - $('html, body').animate({ | |
| 1177 | - scrollTop: errorField.offset().top - 100 | |
| 1178 | - }, 300); | |
| 1179 | - } | |
| 1180 | - return; | |
| 1181 | - } | |
| 1182 | - | |
| 1183 | - $saveBtn.prop('disabled', true); | |
| 1184 | - $saveBtn.find('.save-text').text('Saving...'); | |
| 1185 | - $saveBtn.find('.save-spinner').show(); | |
| 1186 | - $message.hide(); | |
| 1187 | - | |
| 1188 | - $.ajax({ | |
| 1189 | - url: ajaxurl, | |
| 1190 | - type: 'POST', | |
| 1191 | - data: formData, | |
| 1192 | - success: function(response) { | |
| 1193 | - if (response.success) { | |
| 1194 | - showMessage(response.data.message || 'Settings saved successfully!', 'success'); | |
| 1195 | - $saveBtn.prop('disabled', false); | |
| 1196 | - $saveBtn.find('.save-text').text('Save Settings'); | |
| 1197 | - $saveBtn.find('.save-spinner').hide(); | |
| 1198 | - setTimeout(function() { | |
| 1199 | - checkServerLimits(); | |
| 1200 | - }, 100); | |
| 1201 | - $('html, body').animate({ | |
| 1202 | - scrollTop: $form.offset().top - 100 | |
| 1203 | - }, 300); | |
| 1204 | - } else { | |
| 1205 | - showMessage(response.data.message || 'Error saving settings.', 'error'); | |
| 1206 | - $saveBtn.prop('disabled', false); | |
| 1207 | - $saveBtn.find('.save-text').text('Save Settings'); | |
| 1208 | - $saveBtn.find('.save-spinner').hide(); | |
| 1209 | - $('html, body').animate({ | |
| 1210 | - scrollTop: $message.offset().top - 100 | |
| 1211 | - }, 300); | |
| 1212 | - } | |
| 1213 | - }, | |
| 1214 | - error: function() { | |
| 1215 | - showMessage('An error occurred while saving settings. Please try again.', 'error'); | |
| 1216 | - $saveBtn.prop('disabled', false); | |
| 1217 | - $saveBtn.find('.save-text').text('Save Settings'); | |
| 1218 | - $saveBtn.find('.save-spinner').hide(); | |
| 1219 | - $('html, body').animate({ | |
| 1220 | - scrollTop: $message.offset().top - 100 | |
| 1221 | - }, 300); | |
| 1222 | - } | |
| 1223 | - }); | |
| 1224 | - }); | |
| 1225 | - | |
| 1226 | - function showMessage(text, type) { | |
| 1227 | - $message.removeClass('notice-success notice-error') | |
| 1228 | - .addClass('notice-' + type) | |
| 1229 | - .css({ | |
| 1230 | - 'background': type === 'success' ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)', | |
| 1231 | - 'border': '1px solid ' + (type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'), | |
| 1232 | - 'color': type === 'success' ? '#22c55e' : '#ef4444', | |
| 1233 | - 'padding': '12px 16px', | |
| 1234 | - 'border-radius': '6px', | |
| 1235 | - 'font-size': '14px', | |
| 1236 | - 'line-height': '1.5', | |
| 1237 | - 'display': 'block' | |
| 1238 | - }) | |
| 1239 | - .html('<strong style="margin-right: 8px;">' + (type === 'success' ? '✓' : '✗') + '</strong>' + text) | |
| 1240 | - .show(); | |
| 1241 | - | |
| 1242 | - if (type === 'success') { | |
| 1243 | - setTimeout(function() { | |
| 1244 | - $message.fadeOut(300); | |
| 1245 | - }, 5000); | |
| 1246 | - } | |
| 1247 | - } | |
| 1248 | - }); | |
| 1249 | - </script> | |
| 1250 | 1119 | <?php |
| 1120 | + // Real-time validation and AJAX save for this form live in | |
| 1121 | + // admin/js/metasync-execution-settings.js (enqueued on this page). | |
| 1122 | + // The previous inline duplicate was removed to avoid a double-bound | |
| 1123 | + // submit handler that fired the save request twice. | |
| 1124 | + ?> | |
| 1125 | + <?php | |
| 1251 | 1126 | } |
| 1252 | 1127 | |
| 1253 | 1128 | public function get_field_tooltips() { |
| 1254 | 1129 | $plugin_name = Metasync::get_effective_plugin_name(); |
| @@ -1254,36 +1129,73 @@ | ||
| 1254 | 1129 | $plugin_name = Metasync::get_effective_plugin_name(); |
| 1255 | 1130 | $otto_name = Metasync::get_whitelabel_otto_name(); |
| 1256 | 1131 | |
| 1257 | 1132 | return array( |
| 1258 | - 'searchatlas_api_key' => sprintf('Connect your WordPress site to your %s dashboard to retrieve your API key and OTTO UUID. This does not create a WordPress login session.', $plugin_name), | |
| 1133 | + 'searchatlas_api_key' => sprintf('Connect your WordPress site to your %1$s dashboard to retrieve your API key and %2$s UUID. This does not create a WordPress login session.', $plugin_name, $otto_name), | |
| 1259 | 1134 | 'apikey' => sprintf('Auto-generated authentication token used for secure API communication between your WordPress site and %s services. You can refresh this token if needed for security purposes.', $plugin_name), |
| 1260 | 1135 | 'otto_pixel_uuid' => sprintf('Your unique %s tracking pixel identifier. This UUID is used to track %s modifications and analytics on your website pages.', $otto_name, $otto_name), |
| 1261 | 1136 | 'otto_disable_on_loggedin' => sprintf('Disable %s modifications when you are logged in to WordPress. This allows you to see and edit the original content without %s\'s enhancements during editing sessions.', $otto_name, $otto_name), |
| 1262 | 1137 | 'otto_disable_preview_button' => sprintf('Hide the %s frontend toolbar that displays the status indicator, preview button, and debug button. Enable this for a cleaner frontend experience.', $otto_name), |
| 1138 | + 'seo_priority' => sprintf('Choose which value appears first on a page when both your custom SEO value (title, description or focus keyword, edited on the post/page screen) and an approved %s suggestion are available. This changes what visitors see; it does not delete or overwrite saved values.', $otto_name), | |
| 1263 | 1139 | 'otto_wp_rocket_compat' => sprintf('WP Rocket Compatibility Mode: Controls how %s interacts with WP Rocket. "Auto" (recommended) allows both to work together by avoiding DONOTCACHEPAGE constant unless necessary for Brizy pages or SG Optimizer conflicts. This ensures WP Rocket\'s JavaScript delay and optimization features continue working.', $otto_name), |
| 1140 | + 'otto_sg_optimizer_compat' => sprintf('SiteGround Optimizer Compatibility Mode: Controls how %s renders on SiteGround hosting. "Auto" (recommended) uses the internal HTTP fetch, which protects themes that defer inline CSS to the footer (e.g. Divi). "Buffer Mode" renders in-process with no internal fetch — faster on sites whose pages can never be host-cached, for example when a plugin starts a PHP session on every request (BookingPress sessions are removed automatically, so this does not apply to them).', $otto_name), | |
| 1141 | + 'bookingpress_session_compat' => 'BookingPress Session Compatibility: Controls whether MetaSync removes the PHP session BookingPress starts on every public page, so hosts such as SiteGround can page-cache the site again.', | |
| 1264 | 1142 | 'otto_disable_for_bots' => sprintf('Enable bot detection to automatically skip %s processing for search engine crawlers, SEO tools, and other bots. This reduces unnecessary API calls and improves performance.', $otto_name), |
| 1265 | 1143 | 'otto_bot_whitelist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be processed by %s, even when "Disable for Bots" is enabled. For example: Googlebot, Bingbot. This allows you to ensure specific search engines always see your optimized content.', $otto_name), |
| 1266 | 1144 | 'otto_bot_blacklist' => sprintf('Enter bot names or user-agent patterns (one per line) that should always be blocked from %s processing, regardless of other settings. For example: BadBot, MaliciousCrawler. Use this to exclude problematic crawlers or unwanted traffic sources.', $otto_name), |
| 1267 | 1145 | 'otto_bot_statistics_link' => 'View detailed bot detection statistics including total hits, API calls saved, breakdown by bot type, and unique bot entries with hit counts.', |
| 1268 | - 'disable_common_robots_metabox' => 'Hide the Common Robots meta box from post and page edit screens. This removes the robots meta tag controls (index/noindex, follow/nofollow) from the editor interface.', | |
| 1269 | - 'disable_advance_robots_metabox' => 'Hide the Advanced Robots meta box from post and page edit screens. This removes advanced robots directives like max-snippet, max-image-preview, and max-video-preview settings.', | |
| 1270 | - 'disable_redirection_metabox' => 'Hide the Redirection meta box from post and page edit screens. This removes the URL redirect configuration options from the editor interface.', | |
| 1271 | - 'disable_canonical_metabox' => 'Hide the Canonical URL meta box from post and page edit screens. This removes the canonical URL override field from the editor interface.', | |
| 1272 | - 'disable_social_opengraph_metabox' => 'Hide the Social Media & Open Graph meta box from post and page edit screens. This removes Facebook, Twitter, and other social media meta tag controls from the editor.', | |
| 1273 | - 'disable_schema_markup_metabox' => 'Hide the Schema Markup meta box from post and page edit screens. This removes the structured data (Article, FAQ, Product, Recipe, etc.) configuration from the editor interface.', | |
| 1274 | - 'open_external_links' => 'Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by Otto.', | |
| 1146 | + 'disable_common_robots_metabox' => sprintf('Hide the Common Robots meta box and stop %1$s emitting index/noindex and follow/nofollow directives on the front end. %1$s also stops overriding WordPress core or another SEO plugin\'s robots tag. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1147 | + 'disable_advance_robots_metabox' => sprintf('Hide the Advanced Robots meta box and stop %1$s emitting max-snippet, max-image-preview and max-video-preview directives, including the site-wide advanced defaults. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1148 | + 'disable_redirection_metabox' => 'Hide the Redirection meta box and stop redirects configured through it from running. Rules added by hand on the Redirections screen keep working. Saved redirects are kept and resume if you re-enable this.', | |
| 1149 | + 'disable_canonical_metabox' => sprintf('Hide the Canonical meta box and stop %1$s emitting or overriding the canonical URL on every render path. WordPress core and third-party canonicals are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1150 | + 'disable_social_opengraph_metabox' => sprintf('Hide the Social Media & Open Graph meta box and stop %1$s emitting any Open Graph, Twitter or article tags. Tags from another SEO plugin are left in place. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1151 | + 'disable_schema_markup_metabox' => sprintf('Hide the Schema Markup meta box and stop %1$s emitting any JSON-LD, including the site-wide Local SEO markup and OTTO structured data. Breadcrumb markup has its own setting. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1152 | + 'disable_language_alternates_metabox' => sprintf('Hide the Language Alternates (hreflang) panel in the block editor and stop %1$s emitting any hreflang tags on the front end, including the auto-detected WPML entries. While disabled, %1$s does not manage or override this data — another SEO plugin, or WPML itself, is free to handle it. Saved values are kept and restored if you re-enable this.', Metasync::get_effective_plugin_name('MetaSync')), | |
| 1153 | + 'disable_seo_metabox' => 'Hide the SEO Title & Meta Description meta box from post and page edit screens. This removes the Classic editor SEO fields (the Gutenberg sidebar is unaffected).', | |
| 1154 | + 'open_external_links' => sprintf('Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by %s.', $otto_name), | |
| 1275 | 1155 | 'content_genius_sync_roles' => 'Select which WordPress user roles should be synchronized with Content Genius. This determines which users will have their profiles and permissions synced for content collaboration.', |
| 1276 | 1156 | 'permalink_structure' => sprintf('Displays your current WordPress permalink structure. %s works best with pretty permalinks (not "Plain"). If you see a warning, visit Settings > Permalinks to change your structure.', $plugin_name), |
| 1277 | 1157 | 'hide_dashboard_framework' => sprintf('Hide the main %s dashboard from the WordPress admin menu. This is useful if you want to reduce menu clutter but still keep the plugin active.', $plugin_name), |
| 1278 | 1158 | 'show_admin_bar_status' => sprintf('Display the %s status indicator in the WordPress admin bar at the top of your screen. This provides quick visibility of plugin status and key metrics.', $plugin_name), |
| 1279 | - 'enable_auto_updates' => sprintf('Allow WordPress to automatically update the %s plugin when new versions are released. Recommended for security patches, but you may prefer manual updates for major versions.', $plugin_name), | |
| 1280 | 1159 | 'import_external_data' => sprintf('Import your existing SEO settings and metadata from other popular SEO plugins like Yoast, Rank Math, or All in One SEO. This makes migration to %s seamless without losing your SEO data.', $plugin_name), |
| 1281 | - 'import_seo_metadata' => 'Migrate your existing SEO titles and meta descriptions from Yoast, Rank Math, or All in One SEO. This one-click import preserves your search rankings by copying your optimized meta data to MetaSync, even if the source plugin is deactivated.', | |
| 1282 | - 'default_page_builder' => 'Choose how Content Genius stores synced articles. "Gutenberg" works with all themes and page builders. Selecting a specific builder converts content to that builder\'s widget format, which inherits the builder\'s global typography and layout styles.' | |
| 1160 | + 'import_seo_metadata' => sprintf('Migrate your existing SEO titles and meta descriptions from Yoast, Rank Math, or All in One SEO. This one-click import preserves your search rankings by copying your optimized meta data to %s, even if the source plugin is deactivated.', $plugin_name), | |
| 1161 | + 'default_page_builder' => 'Choose how Content Genius stores synced articles. "Gutenberg" works with all themes and page builders. Selecting a specific builder converts content to that builder\'s widget format, which inherits the builder\'s global typography and layout styles.', | |
| 1162 | + 'og_image_dimensions' => 'Adds your share image\'s width, height, and file type to the Open Graph tags so Facebook, LinkedIn, and others render your preview instantly at the right size instead of guessing. Leave on unless another plugin already outputs these.', | |
| 1163 | + 'article_timestamps' => 'Tells social platforms and search engines when each post was first published and last updated, so shares and rich results show accurate dates. Safe to leave enabled.', | |
| 1164 | + 'article_author' => 'Adds a link to the author\'s profile or archive page to each post\'s social tags, so shared articles can show author attribution. You can override the author URL per-post. Turn off if you\'d rather not expose author pages.', | |
| 1165 | + 'article_section' => 'Labels each post with its main topic (its primary category) in the social tags, helping platforms categorize your content. Falls back to the first category if no primary one is set.', | |
| 1166 | + 'article_tags' => 'Adds each of your WordPress post tags as a separate social "tag" so platforms understand the topics a post covers. Turn off if you don\'t want your tags exposed in share metadata.', | |
| 1167 | + 'twitter_image_alt' => 'Adds descriptive alt text to the image shown when your page is shared on X/Twitter, improving accessibility for screen-reader users. Uses your saved alt text or the image\'s own alt attribute.' | |
| 1283 | 1168 | ); |
| 1284 | 1169 | } |
| 1285 | 1170 | |
| 1171 | + public function render_analytics_section() { | |
| 1172 | + $enabled = get_option('metasync_analytics_opt_in', 'no') === 'yes'; | |
| 1173 | + $plugin_name = Metasync::get_effective_plugin_name(); | |
| 1174 | + /* translators: %s: Plugin name. */ | |
| 1175 | + $heading = sprintf(__('Help improve %s', 'metasync'), $plugin_name); | |
| 1176 | + /* translators: %s: Plugin name. */ | |
| 1177 | + $intro = sprintf(__('Allow %s to collect limited usage analytics. This helps us understand which features are used, improve the product, and identify problems.', 'metasync'), $plugin_name); | |
| 1178 | + /* translators: %s: Plugin name. */ | |
| 1179 | + $toggle_label = sprintf(__('Share usage data with %s', 'metasync'), $plugin_name); | |
| 1180 | + ?> | |
| 1181 | + <div class="metasync-analytics-consent-card"> | |
| 1182 | + <div class="metasync-analytics-consent-copy"> | |
| 1183 | + <h3><?php echo esc_html($heading); ?></h3> | |
| 1184 | + <p><?php echo esc_html($intro); ?></p> | |
| 1185 | + <p class="description"><?php esc_html_e('We collect limited technical information such as your site URL, plugin version, WordPress and PHP versions, user role, and feature usage. We do not collect passwords, API keys, customer content, names, or email addresses.', 'metasync'); ?></p> | |
| 1186 | + </div> | |
| 1187 | + <label class="metasync-consent-switch" for="metasync_analytics_opt_in"> | |
| 1188 | + <input type="hidden" name="metasync_analytics_opt_in" value="no" /> | |
| 1189 | + <input type="checkbox" id="metasync_analytics_opt_in" name="metasync_analytics_opt_in" value="yes" <?php checked($enabled); ?> /> | |
| 1190 | + <span class="metasync-consent-slider" aria-hidden="true"></span> | |
| 1191 | + <span class="screen-reader-text"><?php echo esc_html($toggle_label); ?></span> | |
| 1192 | + </label> | |
| 1193 | + <span class="metasync-consent-status" aria-live="polite" data-enabled-label="<?php esc_attr_e('Enabled', 'metasync'); ?>" data-disabled-label="<?php esc_attr_e('Off', 'metasync'); ?>"><?php echo $enabled ? esc_html__('Enabled', 'metasync') : esc_html__('Off', 'metasync'); ?></span> | |
| 1194 | + </div> | |
| 1195 | + <?php | |
| 1196 | + } | |
| 1197 | + | |
| 1286 | 1198 | public function get_field_section($field_id) { |
| 1287 | 1199 | $sections = $this->get_accordion_sections_config(); |
| 1288 | 1200 | |
| 1289 | 1201 | foreach ($sections as $section_key => $section_data) { |
| @@ -1317,9 +1229,9 @@ | ||
| 1317 | 1229 | $content_state = $is_open ? 'open' : 'closed'; |
| 1318 | 1230 | |
| 1319 | 1231 | echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">'; |
| 1320 | 1232 | |
| 1321 | - echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">'; | |
| 1233 | + echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . esc_attr($aria_expanded) . '" aria-controls="' . esc_attr($section_id) . '">'; | |
| 1322 | 1234 | echo '<div class="metasync-accordion-title">'; |
| 1323 | 1235 | echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>'; |
| 1324 | 1236 | echo '<div class="metasync-accordion-text">'; |
| 1325 | 1237 | echo '<h3>' . esc_html($section_data['title']) . '</h3>'; |
| @@ -1330,9 +1242,9 @@ | ||
| 1330 | 1242 | echo '<span class="toggle-icon">▼</span>'; |
| 1331 | 1243 | echo '</button>'; |
| 1332 | 1244 | echo '</div>'; |
| 1333 | 1245 | |
| 1334 | - echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">'; | |
| 1246 | + echo '<div class="metasync-accordion-content" id="' . esc_attr($section_id) . '" data-state="' . esc_attr($content_state) . '">'; | |
| 1335 | 1247 | |
| 1336 | 1248 | if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) { |
| 1337 | 1249 | call_user_func($section_data['render_callback']); |
| 1338 | 1250 | } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) { |
| @@ -1347,9 +1259,9 @@ | ||
| 1347 | 1259 | echo '<tr>'; |
| 1348 | 1260 | echo '<th scope="row">'; |
| 1349 | 1261 | if (!empty($field['title'])) { |
| 1350 | 1262 | echo '<div class="metasync-field-label-wrapper">'; |
| 1351 | - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>'; | |
| 1263 | + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>'; | |
| 1352 | 1264 | |
| 1353 | 1265 | if (isset($tooltips[$field_id])) { |
| 1354 | 1266 | echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">'; |
| 1355 | 1267 | echo '<svg class="metasync-info-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'; |
| @@ -1395,13 +1307,22 @@ | ||
| 1395 | 1307 | return; |
| 1396 | 1308 | } |
| 1397 | 1309 | |
| 1398 | 1310 | echo '<table class="form-table" role="presentation">'; |
| 1311 | + $llms_tooltips = array( | |
| 1312 | + 'llms_txt_enabled' => 'Publishes a file at yoursite.com/llms.txt listing your key pages so AI assistants like ChatGPT and Perplexity can find and cite your content. Safe to leave off if you don\'t want AI crawlers guided to your pages.', | |
| 1313 | + 'llms_full_enabled' => 'Also publishes a larger file with the full text of your posts (not just links). This exposes complete article content and can be large — keep the post limit low on smaller hosting plans.', | |
| 1314 | + ); | |
| 1399 | 1315 | foreach ($wp_settings_fields[$page][$section] as $field) { |
| 1400 | 1316 | echo '<tr>'; |
| 1401 | 1317 | echo '<th scope="row">'; |
| 1402 | 1318 | if (!empty($field['title'])) { |
| 1403 | - echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>'; | |
| 1319 | + echo '<div class="metasync-field-label-wrapper">'; | |
| 1320 | + echo '<label for="' . esc_attr($field['id']) . '">' . esc_html($field['title']) . '</label>'; | |
| 1321 | + if (isset($llms_tooltips[$field['id']])) { | |
| 1322 | + Metasync::render_tooltip_icon($field['id'], $llms_tooltips[$field['id']]); | |
| 1323 | + } | |
| 1324 | + echo '</div>'; | |
| 1404 | 1325 | } |
| 1405 | 1326 | echo '</th>'; |
| 1406 | 1327 | echo '<td>'; |
| 1407 | 1328 | call_user_func($field['callback'], $field['args']); |
| @@ -1451,31 +1372,32 @@ | ||
| 1451 | 1372 | $display_value = $current_token; |
| 1452 | 1373 | $status_message = 'Plugin Auth Token is active and ready for authentication.'; |
| 1453 | 1374 | $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.'; |
| 1454 | 1375 | } else { |
| 1455 | - $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name()); | |
| 1456 | - $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . esc_html(Metasync::get_effective_plugin_name()) . '".'; | |
| 1376 | + $display_value = 'Auto-generated when connecting to ' . Metasync::get_effective_plugin_name(); | |
| 1377 | + $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . Metasync::get_effective_plugin_name() . '".'; | |
| 1457 | 1378 | $refresh_help = 'You can also manually generate a token by clicking refresh.'; |
| 1458 | 1379 | } |
| 1459 | 1380 | |
| 1381 | + printf('<div class="metasync-auth-token-row">'); | |
| 1460 | 1382 | printf( |
| 1461 | - '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" readonly="readonly" /> ', | |
| 1383 | + '<input type="text" id="apikey" name="' . esc_attr(Metasync_Admin::option_key) . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />', | |
| 1462 | 1384 | esc_attr($display_value) |
| 1463 | 1385 | ); |
| 1464 | - | |
| 1465 | - printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary" style="margin-left: 10px;"><span class="dashicons dashicons-controls-repeat" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Refresh Token</button>'); | |
| 1466 | - printf('<p class="description">%s %s</p>', $status_message, $refresh_help); | |
| 1386 | + printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary metasync-refresh-token-btn"><span class="dashicons dashicons-controls-repeat"></span> Refresh Token</button>'); | |
| 1387 | + printf('</div>'); | |
| 1388 | + printf('<p class="description">%s %s</p>', esc_html($status_message), esc_html($refresh_help)); | |
| 1467 | 1389 | } |
| 1468 | 1390 | |
| 1469 | 1391 | public function linkgraph_token_callback() |
| 1470 | 1392 | { |
| 1471 | 1393 | printf( |
| 1472 | - '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />', | |
| 1394 | + '<input type="text" id="linkgraph_token" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />', | |
| 1473 | 1395 | isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : '' |
| 1474 | 1396 | ); |
| 1475 | 1397 | |
| 1476 | 1398 | printf( |
| 1477 | - '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />', | |
| 1399 | + '<input type="text" id="linkgraph_customer_id" name="' . esc_attr(Metasync_Admin::option_key) . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />', | |
| 1478 | 1400 | isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : '' |
| 1479 | 1401 | ); |
| 1480 | 1402 | |
| 1481 | 1403 | ?> |
| @@ -1491,10 +1413,16 @@ | ||
| 1491 | 1413 | { |
| 1492 | 1414 | if(empty($datetime)){ |
| 1493 | 1415 | return ""; |
| 1494 | 1416 | } |
| 1495 | - $now = new DateTime; | |
| 1496 | - $ago = new DateTime($datetime); | |
| 1417 | + # Stored timestamps use current_time('mysql') which is the WP site's | |
| 1418 | + # local timezone (no offset marker on the string). Constructing | |
| 1419 | + # DateTime without a timezone would default to UTC and produce a | |
| 1420 | + # bogus diff equal to the WP timezone offset (QA: "4 hours ago" | |
| 1421 | + # after a 4-minute sync on non-UTC sites). | |
| 1422 | + $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC'); | |
| 1423 | + $now = new DateTime('now', $wp_tz); | |
| 1424 | + $ago = new DateTime($datetime, $wp_tz); | |
| 1497 | 1425 | |
| 1498 | 1426 | $diff = $now->diff($ago); |
| 1499 | 1427 | |
| 1500 | 1428 | $string = [ |
| @@ -1519,12 +1447,26 @@ | ||
| 1519 | 1447 | } |
| 1520 | 1448 | |
| 1521 | 1449 | public function searchatlas_api_key_callback() |
| 1522 | 1450 | { |
| 1523 | - $current_api_key = isset(Metasync::get_option('general')['searchatlas_api_key']) ? esc_attr(Metasync::get_option('general')['searchatlas_api_key']) : ''; | |
| 1451 | + // Whether a key is stored at rest (encrypted or legacy plaintext). | |
| 1452 | + $stored_api_key = Metasync::get_option('general')['searchatlas_api_key'] ?? ''; | |
| 1453 | + $has_api_key = ($stored_api_key !== ''); | |
| 1454 | + | |
| 1455 | + // Decrypted key for masking only. Never rendered in cleartext. | |
| 1456 | + // false => an encrypted key exists but cannot be decrypted (salts changed). | |
| 1457 | + $decrypted_api_key = Metasync::get_searchatlas_api_key(); | |
| 1458 | + $decrypt_failed = ($decrypted_api_key === false); | |
| 1459 | + | |
| 1460 | + // Build a masked representation: bullets + last 4 chars. The cleartext | |
| 1461 | + // key is never emitted into the page. | |
| 1462 | + $masked_api_key = ''; | |
| 1463 | + if (!$decrypt_failed && $decrypted_api_key !== '') { | |
| 1464 | + $masked_api_key = str_repeat('*', 8) . substr($decrypted_api_key, -4); | |
| 1465 | + } | |
| 1466 | + | |
| 1524 | 1467 | $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : ''; |
| 1525 | - | |
| 1526 | - $has_api_key = !empty($current_api_key); | |
| 1468 | + | |
| 1527 | 1469 | $has_otto_uuid = !empty($otto_uuid); |
| 1528 | 1470 | |
| 1529 | 1471 | $is_fully_connected = $this->admin_instance->is_heartbeat_connected(); |
| 1530 | 1472 | |
| @@ -1578,11 +1520,11 @@ | ||
| 1578 | 1520 | printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">'); |
| 1579 | 1521 | printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name())); |
| 1580 | 1522 | printf('• The authentication window will open in a popup - please allow popups<br/>'); |
| 1581 | 1523 | printf('• The process typically takes 15-30 seconds to complete<br/>'); |
| 1582 | - printf('• Your API key will be automatically filled in the field below<br/>'); | |
| 1524 | + printf('• Your API key will be stored securely and shown masked below<br/>'); | |
| 1583 | 1525 | printf('• If you encounter issues, try disabling ad blockers temporarily<br/>'); |
| 1584 | - printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL); | |
| 1526 | + printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', esc_attr(Metasync::SUPPORT_EMAIL), esc_html(Metasync::SUPPORT_EMAIL)); | |
| 1585 | 1527 | printf('</div>'); |
| 1586 | 1528 | printf('</details>'); |
| 1587 | 1529 | printf('</div>'); |
| 1588 | 1530 | |
| @@ -1597,30 +1539,42 @@ | ||
| 1597 | 1539 | printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name())); |
| 1598 | 1540 | } |
| 1599 | 1541 | printf('</label>'); |
| 1600 | 1542 | |
| 1543 | + // Read-only masked display. The API key is managed via one-click | |
| 1544 | + // authentication only; the cleartext key is never rendered into the | |
| 1545 | + // page HTML (View Source / DOM). No form input is emitted, so saving | |
| 1546 | + // settings cannot overwrite or expose the stored key. | |
| 1547 | + $display_has_key = (!$decrypt_failed && $masked_api_key !== '') ? '1' : '0'; | |
| 1601 | 1548 | printf( |
| 1602 | - '<input type="text" id="searchatlas-api-key" name="' . Metasync_Admin::option_key . '[general][searchatlas_api_key]" value="%s" size="40" class="regular-text" placeholder="Your API key will appear here after authentication" />', | |
| 1603 | - $current_api_key | |
| 1549 | + '<div id="searchatlas-api-key-display" class="metasync-api-key-masked" data-has-key="%s" style="display:inline-flex; align-items:center; min-width:320px; height:42px; padding:0 14px; font-family:monospace; font-size:15px; letter-spacing:4px; background:var(--dashboard-input-bg, #22272e); border:1px solid var(--dashboard-border, #3a424d); border-radius:6px; color:var(--dashboard-text-primary, #e6e8eb); cursor:default; user-select:none;">', | |
| 1550 | + esc_attr($display_has_key) | |
| 1604 | 1551 | ); |
| 1605 | - | |
| 1552 | + if ($decrypt_failed) { | |
| 1553 | + printf('<em style="color:#b32d2e; font-family:sans-serif; letter-spacing:normal; font-style:normal; font-size:14px;">Stored API key could not be read. Please re-authenticate above.</em>'); | |
| 1554 | + } elseif ($masked_api_key !== '') { | |
| 1555 | + echo esc_html($masked_api_key); | |
| 1556 | + } else { | |
| 1557 | + printf('<em style="color:#646970;">Not configured</em>'); | |
| 1558 | + } | |
| 1559 | + printf('</div>'); | |
| 1560 | + | |
| 1606 | 1561 | printf('<p class="description" style="margin-top: 8px;">'); |
| 1607 | - if ($is_fully_connected) { | |
| 1608 | - printf('Your %s API key for secure communication with the platform. Use the authentication button above to refresh or change accounts.', esc_html(Metasync::get_effective_plugin_name())); | |
| 1562 | + if ($decrypt_failed) { | |
| 1563 | + printf('Your stored API key could not be decrypted (your site security keys may have changed). Please re-authenticate above to reconnect.'); | |
| 1564 | + } elseif ($is_fully_connected) { | |
| 1565 | + printf('Your %s API key is stored securely and used for communication with the platform. Use the authentication button above to refresh or change accounts.', esc_html(Metasync::get_effective_plugin_name())); | |
| 1609 | 1566 | } elseif ($has_api_key && !$has_otto_uuid) { |
| 1610 | 1567 | printf('Your API key is configured but %s UUID is missing. Re-authenticate above to complete the setup and enable dashboard access.', esc_html(Metasync::get_whitelabel_otto_name())); |
| 1611 | 1568 | } else { |
| 1612 | - printf('This field will be automatically populated when you authenticate using the button above. You can also manually enter your API key if you have one.'); | |
| 1569 | + printf('Use the authentication button above to securely connect your account. Your API key is stored encrypted and shown masked here.'); | |
| 1613 | 1570 | } |
| 1614 | 1571 | printf('</p>'); |
| 1615 | 1572 | |
| 1616 | - ?> | |
| 1617 | - <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;"> | |
| 1618 | - <strong>Manual Authentication:</strong> If you manually enter your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> API Key and OTTO UUID, you also consent to the same AI-powered automation permissions described above. | |
| 1619 | - </p> | |
| 1620 | - <?php | |
| 1573 | + printf('</div>'); | |
| 1621 | 1574 | |
| 1622 | - printf('</div>'); | |
| 1575 | + // The OTTO Pixel UUID is shown once, by the read-only Settings-API | |
| 1576 | + // field in the OTTO Server-Side Rendering section; do not repeat it here. | |
| 1623 | 1577 | |
| 1624 | 1578 | if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){ |
| 1625 | 1579 | $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp']; |
| 1626 | 1580 | printf( |
| @@ -1625,9 +1579,9 @@ | ||
| 1625 | 1579 | $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp']; |
| 1626 | 1580 | printf( |
| 1627 | 1581 | '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>', |
| 1628 | 1582 | esc_attr($timestamp), |
| 1629 | - $this->time_elapsed_string($timestamp) | |
| 1583 | + esc_html($this->time_elapsed_string($timestamp)) | |
| 1630 | 1584 | ); |
| 1631 | 1585 | |
| 1632 | 1586 | |
| 1633 | 1587 | } |
| @@ -1635,9 +1589,9 @@ | ||
| 1635 | 1589 | |
| 1636 | 1590 | public function bing_site_verification_callback() |
| 1637 | 1591 | { |
| 1638 | 1592 | printf( |
| 1639 | - '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />', | |
| 1593 | + '<input type="text" id="bing_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][bing_site_verification]" value="%s" size="50" />', | |
| 1640 | 1594 | isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : '' |
| 1641 | 1595 | ); |
| 1642 | 1596 | |
| 1643 | 1597 | printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> '); |
| @@ -1646,9 +1600,9 @@ | ||
| 1646 | 1600 | |
| 1647 | 1601 | public function yandex_site_verification_callback() |
| 1648 | 1602 | { |
| 1649 | 1603 | printf( |
| 1650 | - '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />', | |
| 1604 | + '<input type="text" id="yandex_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][yandex_site_verification]" value="%s" size="50" />', | |
| 1651 | 1605 | isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : '' |
| 1652 | 1606 | ); |
| 1653 | 1607 | |
| 1654 | 1608 | printf(' <br> <span class="description"> Enter Yandex verification code: </span>'); |
| @@ -1657,9 +1611,9 @@ | ||
| 1657 | 1611 | |
| 1658 | 1612 | public function google_site_verification_callback() |
| 1659 | 1613 | { |
| 1660 | 1614 | printf( |
| 1661 | - '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />', | |
| 1615 | + '<input type="text" id="google_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][google_site_verification]" value="%s" size="50" />', | |
| 1662 | 1616 | isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : '' |
| 1663 | 1617 | ); |
| 1664 | 1618 | |
| 1665 | 1619 | printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>'); |
| @@ -1668,9 +1622,9 @@ | ||
| 1668 | 1622 | |
| 1669 | 1623 | public function pinterest_site_verification_callback() |
| 1670 | 1624 | { |
| 1671 | 1625 | printf( |
| 1672 | - '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />', | |
| 1626 | + '<input type="text" id="pinterest_site_verification" name="' . esc_attr(Metasync_Admin::option_key) . '[searchengines][pinterest_site_verification]" value="%s" size="50" />', | |
| 1673 | 1627 | isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : '' |
| 1674 | 1628 | ); |
| 1675 | 1629 | |
| 1676 | 1630 | printf(' <br> <span class="description"> Enter Pinterest verification code: </span>'); |
| @@ -1679,15 +1633,21 @@ | ||
| 1679 | 1633 | |
| 1680 | 1634 | public function local_seo_person_organization_callback() |
| 1681 | 1635 | { |
| 1682 | 1636 | $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? ''; |
| 1637 | + $valid_person_organizations = array('Person', 'Organization'); | |
| 1683 | 1638 | ?> |
| 1684 | - <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>"> | |
| 1685 | - <?php | |
| 1686 | - printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization))); | |
| 1687 | - printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization))); | |
| 1688 | - ?> | |
| 1689 | - </select> | |
| 1639 | + <div class="metasync-type-select-wrap"> | |
| 1640 | + <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>"> | |
| 1641 | + <?php | |
| 1642 | + if (!in_array($person_organization, $valid_person_organizations, true)) { | |
| 1643 | + printf('<option value="0" selected="selected">Select Type</option>'); | |
| 1644 | + } | |
| 1645 | + printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization))); | |
| 1646 | + printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization))); | |
| 1647 | + ?> | |
| 1648 | + </select> | |
| 1649 | + </div> | |
| 1690 | 1650 | <?php |
| 1691 | 1651 | printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>'); |
| 1692 | 1652 | } |
| 1693 | 1653 | |
| @@ -1693,10 +1653,10 @@ | ||
| 1693 | 1653 | |
| 1694 | 1654 | public function local_seo_name_callback() |
| 1695 | 1655 | { |
| 1696 | 1656 | printf( |
| 1697 | - '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />', | |
| 1698 | - isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo() | |
| 1657 | + '<input type="text" id="local_seo_name" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_name]" value="%s" size="50" />', | |
| 1658 | + isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : esc_attr(get_bloginfo()) | |
| 1699 | 1659 | ); |
| 1700 | 1660 | |
| 1701 | 1661 | printf(' <br> <span class="description"> Your name or company name </span>'); |
| 1702 | 1662 | } |
| @@ -1704,31 +1664,82 @@ | ||
| 1704 | 1664 | public function local_seo_logo_callback() |
| 1705 | 1665 | { |
| 1706 | 1666 | $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? ''; |
| 1707 | 1667 | |
| 1668 | + // Rows saved by the legacy sanitizer hold the corrupted form of | |
| 1669 | + // an attachment ID ("http://45589"). Repair it before anything reads | |
| 1670 | + // the value — both so the preview resolves and so the hidden input | |
| 1671 | + // re-submits the recovered ID instead of the corrupted string, which | |
| 1672 | + // would otherwise survive every subsequent Save Settings. | |
| 1673 | + $local_seo_logo = metasync_repair_scheme_prefixed_media_id($local_seo_logo); | |
| 1674 | + | |
| 1708 | 1675 | printf( |
| 1709 | - '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />', | |
| 1710 | - isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : '' | |
| 1676 | + '<input type="hidden" id="local_seo_logo" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_logo]" value="%s" size="50" />', | |
| 1677 | + esc_attr((string) $local_seo_logo) | |
| 1711 | 1678 | ); |
| 1712 | 1679 | |
| 1713 | - printf(' <br> <input class="button-secondary" type="button" id="logo_upload_button" value="Add or Upload File">'); | |
| 1680 | + printf(' <br> <input class="button-secondary no-loading" type="button" id="logo_upload_button" value="Add or Upload File">'); | |
| 1714 | 1681 | |
| 1715 | 1682 | printf(' <br><br> <span class="description bold"> Min Size: 160Χ90px, Max Size: 1920X1080px. </span> <br> <span class="description"> A squared image is preferred by the search engines. </span> <br><br> '); |
| 1716 | 1683 | |
| 1717 | - printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? ''); | |
| 1684 | + printf('<span class="metasync-logo-preview-wrap">'); | |
| 1718 | 1685 | |
| 1719 | - $button_type = 'hidden'; | |
| 1720 | - if ($local_seo_logo) { | |
| 1721 | - $button_type = 'button'; | |
| 1686 | + // The stored value may be an attachment ID or a raw URL, so resolve | |
| 1687 | + // both the way the front-end schema output already does. Anything that | |
| 1688 | + // resolves to nothing — a deleted attachment, a legacy value mangled by | |
| 1689 | + // an earlier save — must render no preview at all, rather than an empty | |
| 1690 | + // src next to an orphaned remove button. | |
| 1691 | + $logo_url = $this->resolve_media_url($local_seo_logo, 'medium'); | |
| 1692 | + | |
| 1693 | + printf( | |
| 1694 | + '<img src="%s" id="local_seo_business_logo" width="300"%s>', | |
| 1695 | + esc_url($logo_url), | |
| 1696 | + $logo_url ? '' : ' style="display:none"' | |
| 1697 | + ); | |
| 1698 | + | |
| 1699 | + $button_type = $logo_url ? 'button' : 'hidden'; | |
| 1700 | + printf('<input type="%s" class="button-secondary no-loading metasync-logo-remove-btn" id="local_seo_logo_close_btn" value="X">', esc_attr($button_type)); | |
| 1701 | + printf('</span>'); | |
| 1702 | + } | |
| 1703 | + | |
| 1704 | + /** | |
| 1705 | + * Resolve a stored media setting to a displayable URL. | |
| 1706 | + * | |
| 1707 | + * These settings accept either a media-library attachment ID or a raw URL; | |
| 1708 | + * returns an empty string when the value resolves to neither, so callers | |
| 1709 | + * can distinguish "no image" from "image we could not resolve". | |
| 1710 | + * | |
| 1711 | + * @param mixed $value Attachment ID or URL as stored in the options. | |
| 1712 | + * @param string $size Registered image size to request for an attachment. | |
| 1713 | + * @return string Resolved URL, or '' when there is nothing to show. | |
| 1714 | + */ | |
| 1715 | + private function resolve_media_url($value, $size = 'medium') | |
| 1716 | + { | |
| 1717 | + if (empty($value)) { | |
| 1718 | + return ''; | |
| 1722 | 1719 | } |
| 1723 | - printf('<input type="%s" class="button-secondary" id="local_seo_logo_close_btn" value="X">', $button_type); | |
| 1720 | + | |
| 1721 | + // A row saved by the legacy sanitizer stores a corrupted attachment ID ("http://45589"), | |
| 1722 | + // which is neither resolvable as an ID nor a usable image URL. | |
| 1723 | + $value = metasync_repair_scheme_prefixed_media_id($value); | |
| 1724 | + | |
| 1725 | + if (empty($value)) { | |
| 1726 | + return ''; | |
| 1727 | + } | |
| 1728 | + | |
| 1729 | + if (is_numeric($value)) { | |
| 1730 | + $url = wp_get_attachment_image_url((int) $value, $size); | |
| 1731 | + return $url ? $url : ''; | |
| 1732 | + } | |
| 1733 | + | |
| 1734 | + return (string) $value; | |
| 1724 | 1735 | } |
| 1725 | 1736 | |
| 1726 | 1737 | public function local_seo_url_callback() |
| 1727 | 1738 | { |
| 1728 | 1739 | printf( |
| 1729 | - '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />', | |
| 1730 | - isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url() | |
| 1740 | + '<input type="text" id="local_seo_url" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_url]" value="%s" size="50" />', | |
| 1741 | + isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : esc_attr(home_url()) | |
| 1731 | 1742 | ); |
| 1732 | 1743 | |
| 1733 | 1744 | printf(' <br> <span class="description"> URL of the item. </span>'); |
| 1734 | 1745 | } |
| @@ -1735,9 +1746,9 @@ | ||
| 1735 | 1746 | |
| 1736 | 1747 | public function local_seo_email_callback() |
| 1737 | 1748 | { |
| 1738 | 1749 | printf( |
| 1739 | - '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />', | |
| 1750 | + '<input type="text" id="local_seo_email" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_email]" value="%s" size="50" />', | |
| 1740 | 1751 | isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : '' |
| 1741 | 1752 | ); |
| 1742 | 1753 | |
| 1743 | 1754 | printf(' <br> <span class="description"> Search engines display your email address. </span>'); |
| @@ -1745,9 +1756,9 @@ | ||
| 1745 | 1756 | |
| 1746 | 1757 | public function local_seo_phone_callback() |
| 1747 | 1758 | { |
| 1748 | 1759 | printf( |
| 1749 | - '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />', | |
| 1760 | + '<input type="text" id="local_seo_phone" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_phone]" value="%s" size="50" />', | |
| 1750 | 1761 | isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : '' |
| 1751 | 1762 | ); |
| 1752 | 1763 | |
| 1753 | 1764 | printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>'); |
| @@ -1755,29 +1766,29 @@ | ||
| 1755 | 1766 | |
| 1756 | 1767 | public function local_seo_address_callback() |
| 1757 | 1768 | { |
| 1758 | 1769 | printf( |
| 1759 | - '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>', | |
| 1770 | + '<input type="text" id="local_seo_address_street" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>', | |
| 1760 | 1771 | isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : '' |
| 1761 | 1772 | ); |
| 1762 | 1773 | |
| 1763 | 1774 | printf( |
| 1764 | - '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>', | |
| 1775 | + '<input type="text" id="local_seo_address_locality" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>', | |
| 1765 | 1776 | isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : '' |
| 1766 | 1777 | ); |
| 1767 | 1778 | |
| 1768 | 1779 | printf( |
| 1769 | - '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>', | |
| 1780 | + '<input type="text" id="local_seo_address_region" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>', | |
| 1770 | 1781 | isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : '' |
| 1771 | 1782 | ); |
| 1772 | 1783 | |
| 1773 | 1784 | printf( |
| 1774 | - '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>', | |
| 1785 | + '<input type="text" id="local_seo_address_postalcode" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>', | |
| 1775 | 1786 | isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : '' |
| 1776 | 1787 | ); |
| 1777 | 1788 | |
| 1778 | 1789 | printf( |
| 1779 | - '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>', | |
| 1790 | + '<input type="text" id="local_seo_address_country" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>', | |
| 1780 | 1791 | isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : '' |
| 1781 | 1792 | ); |
| 1782 | 1793 | } |
| 1783 | 1794 | |
| @@ -1792,9 +1803,9 @@ | ||
| 1792 | 1803 | <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>"> |
| 1793 | 1804 | <option value='0'>Select Business Type</option> |
| 1794 | 1805 | <?php |
| 1795 | 1806 | foreach ($types as $type) { |
| 1796 | - printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type); | |
| 1807 | + printf('<option value="%s" %s >%s</option>', esc_attr($type), selected($type, $business_type, false), esc_html($type)); | |
| 1797 | 1808 | } |
| 1798 | 1809 | ?> |
| 1799 | 1810 | </select> |
| 1800 | 1811 | <?php |
| @@ -1818,9 +1829,9 @@ | ||
| 1818 | 1829 | <li> |
| 1819 | 1830 | <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>"> |
| 1820 | 1831 | <?php |
| 1821 | 1832 | foreach ($days_name as $name) { |
| 1822 | - printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name)); | |
| 1833 | + printf('<option value="%s" %s >%s</option>', esc_attr($name), selected($name, $day_name, false), esc_html($name)); | |
| 1823 | 1834 | } |
| 1824 | 1835 | ?> |
| 1825 | 1836 | </select> |
| 1826 | 1837 | <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>"> |
| @@ -1899,9 +1910,9 @@ | ||
| 1899 | 1910 | |
| 1900 | 1911 | public function local_seo_price_range_callback() |
| 1901 | 1912 | { |
| 1902 | 1913 | printf( |
| 1903 | - '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />', | |
| 1914 | + '<input type="text" id="local_seo_price_range" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_price_range]" value="%s" size="50" />', | |
| 1904 | 1915 | isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : '' |
| 1905 | 1916 | ); |
| 1906 | 1917 | printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>'); |
| 1907 | 1918 | } |
| @@ -1914,9 +1925,9 @@ | ||
| 1914 | 1925 | <?php |
| 1915 | 1926 | $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? ''; |
| 1916 | 1927 | $pages = get_pages(); |
| 1917 | 1928 | foreach ($pages as $page) { |
| 1918 | - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title); | |
| 1929 | + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $about_page, false), esc_html($page->post_title)); | |
| 1919 | 1930 | } |
| 1920 | 1931 | ?> |
| 1921 | 1932 | </select> |
| 1922 | 1933 | <?php |
| @@ -1931,9 +1942,9 @@ | ||
| 1931 | 1942 | <?php |
| 1932 | 1943 | $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? ''; |
| 1933 | 1944 | $pages = get_pages(); |
| 1934 | 1945 | foreach ($pages as $page) { |
| 1935 | - printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title); | |
| 1946 | + printf('<option value="%s" %s >%s</option>', absint($page->ID), selected($page->ID, $contact_page, false), esc_html($page->post_title)); | |
| 1936 | 1947 | } |
| 1937 | 1948 | ?> |
| 1938 | 1949 | </select> |
| 1939 | 1950 | <?php |
| @@ -1942,9 +1953,9 @@ | ||
| 1942 | 1953 | |
| 1943 | 1954 | public function local_seo_map_key_callback() |
| 1944 | 1955 | { |
| 1945 | 1956 | printf( |
| 1946 | - '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />', | |
| 1957 | + '<input type="text" id="local_seo_map_key" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_map_key]" value="%s" size="50" />', | |
| 1947 | 1958 | isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : '' |
| 1948 | 1959 | ); |
| 1949 | 1960 | |
| 1950 | 1961 | printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>'); |
| @@ -1952,9 +1963,9 @@ | ||
| 1952 | 1963 | |
| 1953 | 1964 | public function local_seo_geo_coordinates_callback() |
| 1954 | 1965 | { |
| 1955 | 1966 | printf( |
| 1956 | - '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />', | |
| 1967 | + '<input type="text" id="local_seo_geo_coordinates" name="' . esc_attr(Metasync_Admin::option_key) . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />', | |
| 1957 | 1968 | isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : '' |
| 1958 | 1969 | ); |
| 1959 | 1970 | |
| 1960 | 1971 | printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>'); |
| @@ -1962,9 +1973,9 @@ | ||
| 1962 | 1973 | |
| 1963 | 1974 | public function header_snippets_callback() |
| 1964 | 1975 | { |
| 1965 | 1976 | printf( |
| 1966 | - '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>', | |
| 1977 | + '<textarea class="wide-text" id="header_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][header_snippet]" >%s</textarea>', | |
| 1967 | 1978 | isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : '' |
| 1968 | 1979 | ); |
| 1969 | 1980 | } |
| 1970 | 1981 | |
| @@ -1970,163 +1981,13 @@ | ||
| 1970 | 1981 | |
| 1971 | 1982 | public function footer_snippets_callback() |
| 1972 | 1983 | { |
| 1973 | 1984 | printf( |
| 1974 | - '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>', | |
| 1985 | + '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . esc_attr(Metasync_Admin::option_key) . '[codesnippets][footer_snippet]" >%s</textarea>', | |
| 1975 | 1986 | isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : '' |
| 1976 | 1987 | ); |
| 1977 | 1988 | } |
| 1978 | 1989 | |
| 1979 | - public function no_index_posts_callback() | |
| 1980 | - { | |
| 1981 | - printf( | |
| 1982 | - '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />', | |
| 1983 | - isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : '' | |
| 1984 | - ); | |
| 1985 | - | |
| 1986 | - printf(' <br> <span class="description"> Setting empty archives to <code>noindex</code> is useful for avoiding indexation of thin content pages and dilution of page rank. As soon as a post is added, the page is updated to index. </span>'); | |
| 1987 | - } | |
| 1988 | - | |
| 1989 | - public function no_follow_links_callback() | |
| 1990 | - { | |
| 1991 | - printf( | |
| 1992 | - '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />', | |
| 1993 | - isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : '' | |
| 1994 | - ); | |
| 1995 | - | |
| 1996 | - printf(' <br> <span class="description"> Automatically add <code>rel="nofollow"</code> attribute to external links appearing in your posts, pages, and other post types. The attribute is dynamically applied when the url is displayed</span>'); | |
| 1997 | - } | |
| 1998 | - | |
| 1999 | - public function open_external_links_callback() | |
| 2000 | - { | |
| 2001 | - printf( | |
| 2002 | - '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />', | |
| 2003 | - isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : '' | |
| 2004 | - ); | |
| 2005 | - | |
| 2006 | - printf(' <br> <span class="description"> Automatically add <code>target="_blank"</code> attribute to external links appearing in your posts, pages, and other post types. The attribute is applied when the url is displayed.</span>'); | |
| 2007 | - } | |
| 2008 | - | |
| 2009 | - public function add_alt_image_tags_callback() | |
| 2010 | - { | |
| 2011 | - printf( | |
| 2012 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />', | |
| 2013 | - isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : '' | |
| 2014 | - ); | |
| 2015 | - | |
| 2016 | - printf(' <br> <span class="description"> Automatically add <code>alt</code> attribute to Image Tags appearing in your posts, pages, and other post types. The attribute is applied when the content is displayed.</span>'); | |
| 2017 | - } | |
| 2018 | - | |
| 2019 | - public function add_title_image_tags_callback() | |
| 2020 | - { | |
| 2021 | - printf( | |
| 2022 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />', | |
| 2023 | - isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : '' | |
| 2024 | - ); | |
| 2025 | - | |
| 2026 | - printf(' <br> <span class="description"> Automatically add <code>title</code> attribute to Image Tags appearing in your posts, pages, and other post types. The attribute is applied when the content is displayed.</span>'); | |
| 2027 | - } | |
| 2028 | - | |
| 2029 | - public function site_type_callback() | |
| 2030 | - { | |
| 2031 | - | |
| 2032 | - $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? ''; | |
| 2033 | - | |
| 2034 | - $types = [ | |
| 2035 | - ['name' => 'Personal Blog', 'value' => 'blog'], | |
| 2036 | - ['name' => 'Community Blog/News Site', 'value' => 'news'], | |
| 2037 | - ['name' => 'Personal Portfolio', 'value' => 'portfolio'], | |
| 2038 | - ['name' => 'Small Business Site', 'value' => 'business'], | |
| 2039 | - ['name' => 'Webshop', 'value' => 'webshop'], | |
| 2040 | - ['name' => 'Other Personal Website', 'value' => 'otherpersonal'], | |
| 2041 | - ['name' => 'Other Business Website', 'value' => 'otherbusiness'], | |
| 2042 | - ]; | |
| 2043 | - | |
| 2044 | - ?> | |
| 2045 | - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type"> | |
| 2046 | - <?php | |
| 2047 | - foreach ($types as $type) { | |
| 2048 | - printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name'])); | |
| 2049 | - } | |
| 2050 | - ?> | |
| 2051 | - </select> | |
| 2052 | - <?php | |
| 2053 | - | |
| 2054 | - } | |
| 2055 | - | |
| 2056 | - public function site_business_type_callback() | |
| 2057 | - { | |
| 2058 | - | |
| 2059 | - $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? ''; | |
| 2060 | - | |
| 2061 | - $types = self::get_business_types(); | |
| 2062 | - sort($types); | |
| 2063 | - | |
| 2064 | - ?> | |
| 2065 | - <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>"> | |
| 2066 | - <option value='0'>Select Business Type</option> | |
| 2067 | - <?php | |
| 2068 | - foreach ($types as $type) { | |
| 2069 | - printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type)); | |
| 2070 | - } | |
| 2071 | - ?> | |
| 2072 | - </select> | |
| 2073 | - <?php | |
| 2074 | - } | |
| 2075 | - | |
| 2076 | - public function site_company_name_callback() | |
| 2077 | - { | |
| 2078 | - | |
| 2079 | - $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name'); | |
| 2080 | - | |
| 2081 | - printf( | |
| 2082 | - '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />', | |
| 2083 | - $company_name ? $company_name : get_bloginfo('name') | |
| 2084 | - ); | |
| 2085 | - } | |
| 2086 | - | |
| 2087 | - public function site_google_logo_callback() | |
| 2088 | - { | |
| 2089 | - | |
| 2090 | - $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? ''; | |
| 2091 | - | |
| 2092 | - printf( | |
| 2093 | - '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />', | |
| 2094 | - $google_logo | |
| 2095 | - ); | |
| 2096 | - | |
| 2097 | - printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">'); | |
| 2098 | - printf(' <br><br> <span class="description bold"> Min Size: 160X90px, Max Size: 1920X1080px. </span> <br> <span class="description"> A squared image is preferred by the search engines. </span> <br><br> '); | |
| 2099 | - printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? ''); | |
| 2100 | - | |
| 2101 | - $button_type = 'hidden'; | |
| 2102 | - if ($google_logo) { | |
| 2103 | - $button_type = 'button'; | |
| 2104 | - } | |
| 2105 | - printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type); | |
| 2106 | - } | |
| 2107 | - | |
| 2108 | - public function site_social_share_image_callback() | |
| 2109 | - { | |
| 2110 | - | |
| 2111 | - $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? ''; | |
| 2112 | - | |
| 2113 | - printf( | |
| 2114 | - '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />', | |
| 2115 | - $social_share_image | |
| 2116 | - ); | |
| 2117 | - | |
| 2118 | - printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">'); | |
| 2119 | - printf(' <br><br> <span class="description bold"> The recommended image size is 1200 x 630 pixels. </span> <br> <span class="description"> When a featured image or an OpenGraph Image is not set for individual posts/pages/CPTs, this image will be used as a fallback thumbnail when your post is shared on Facebook. </span> <br><br> '); | |
| 2120 | - printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? ''); | |
| 2121 | - | |
| 2122 | - $button_type = 'hidden'; | |
| 2123 | - if ($social_share_image) { | |
| 2124 | - $button_type = 'button'; | |
| 2125 | - } | |
| 2126 | - printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type); | |
| 2127 | - } | |
| 2128 | - | |
| 2129 | 1990 | public function common_robot_meta_tags_callback() |
| 2130 | 1991 | { |
| 2131 | 1992 | $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? ''; |
| 2132 | 1993 | |
| @@ -2132,16 +1993,8 @@ | ||
| 2132 | 1993 | |
| 2133 | 1994 | ?> |
| 2134 | 1995 | <ul class="checkbox-list"> |
| 2135 | 1996 | <li> |
| 2136 | - <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][index]') ?>" id="robots_common1" value="index" <?php isset($common_robots_meta['index']) ? checked('index', $common_robots_meta['index']) : '' ?>> | |
| 2137 | - <label for="robots_common1">Index </br> | |
| 2138 | - <span class="description"> | |
| 2139 | - <span>Search engines to index and show these pages in the search results.</span> | |
| 2140 | - </span> | |
| 2141 | - </label> | |
| 2142 | - </li> | |
| 2143 | - <li> | |
| 2144 | 1997 | <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noindex]') ?>" id="robots_common2" value="noindex" <?php isset($common_robots_meta['noindex']) ? checked('noindex', $common_robots_meta['noindex']) : '' ?>> |
| 2145 | 1998 | <label for="robots_common2">No Index </br> |
| 2146 | 1999 | <span class="description"> |
| 2147 | 2000 | <span>Search engines not indexed and displayed this pages in search engine results</span> |
| @@ -2267,9 +2120,9 @@ | ||
| 2267 | 2120 | |
| 2268 | 2121 | public function global_open_graph_meta_callback() |
| 2269 | 2122 | { |
| 2270 | 2123 | printf( |
| 2271 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />', | |
| 2124 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />', | |
| 2272 | 2125 | isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : '' |
| 2273 | 2126 | ); |
| 2274 | 2127 | printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>'); |
| 2275 | 2128 | } |
| @@ -2276,9 +2129,9 @@ | ||
| 2276 | 2129 | |
| 2277 | 2130 | public function global_facebook_meta_callback() |
| 2278 | 2131 | { |
| 2279 | 2132 | printf( |
| 2280 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />', | |
| 2133 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][facebook_meta_tags]" value="true" %s />', | |
| 2281 | 2134 | isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : '' |
| 2282 | 2135 | ); |
| 2283 | 2136 | printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>'); |
| 2284 | 2137 | } |
| @@ -2285,9 +2138,9 @@ | ||
| 2285 | 2138 | |
| 2286 | 2139 | public function global_twitter_meta_callback() |
| 2287 | 2140 | { |
| 2288 | 2141 | printf( |
| 2289 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />', | |
| 2142 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_meta_tags]" value="true" %s />', | |
| 2290 | 2143 | isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : '' |
| 2291 | 2144 | ); |
| 2292 | 2145 | printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>'); |
| 2293 | 2146 | } |
| @@ -2295,9 +2148,9 @@ | ||
| 2295 | 2148 | public function og_image_dimensions_callback() |
| 2296 | 2149 | { |
| 2297 | 2150 | $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true'; |
| 2298 | 2151 | printf( |
| 2299 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />', | |
| 2152 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][og_image_dimensions]" value="true" %s />', | |
| 2300 | 2153 | $val === 'true' ? 'checked' : '' |
| 2301 | 2154 | ); |
| 2302 | 2155 | printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>'); |
| 2303 | 2156 | } |
| @@ -2305,9 +2158,9 @@ | ||
| 2305 | 2158 | public function article_timestamps_callback() |
| 2306 | 2159 | { |
| 2307 | 2160 | $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true'; |
| 2308 | 2161 | printf( |
| 2309 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />', | |
| 2162 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_timestamps]" value="true" %s />', | |
| 2310 | 2163 | $val === 'true' ? 'checked' : '' |
| 2311 | 2164 | ); |
| 2312 | 2165 | printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>'); |
| 2313 | 2166 | } |
| @@ -2315,9 +2168,9 @@ | ||
| 2315 | 2168 | public function article_author_callback() |
| 2316 | 2169 | { |
| 2317 | 2170 | $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true'; |
| 2318 | 2171 | printf( |
| 2319 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />', | |
| 2172 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_author]" value="true" %s />', | |
| 2320 | 2173 | $val === 'true' ? 'checked' : '' |
| 2321 | 2174 | ); |
| 2322 | 2175 | printf('<span class="description"> Output article:author using the post author website URL or archive URL (overridable per-post via _metasync_og_article_author meta).</span>'); |
| 2323 | 2176 | } |
| @@ -2325,9 +2178,9 @@ | ||
| 2325 | 2178 | public function article_section_callback() |
| 2326 | 2179 | { |
| 2327 | 2180 | $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true'; |
| 2328 | 2181 | printf( |
| 2329 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />', | |
| 2182 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_section]" value="true" %s />', | |
| 2330 | 2183 | $val === 'true' ? 'checked' : '' |
| 2331 | 2184 | ); |
| 2332 | 2185 | printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>'); |
| 2333 | 2186 | } |
| @@ -2335,9 +2188,9 @@ | ||
| 2335 | 2188 | public function article_tags_callback() |
| 2336 | 2189 | { |
| 2337 | 2190 | $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true'; |
| 2338 | 2191 | printf( |
| 2339 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />', | |
| 2192 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][article_tags]" value="true" %s />', | |
| 2340 | 2193 | $val === 'true' ? 'checked' : '' |
| 2341 | 2194 | ); |
| 2342 | 2195 | printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>'); |
| 2343 | 2196 | } |
| @@ -2345,9 +2198,9 @@ | ||
| 2345 | 2198 | public function twitter_image_alt_callback() |
| 2346 | 2199 | { |
| 2347 | 2200 | $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true'; |
| 2348 | 2201 | printf( |
| 2349 | - '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />', | |
| 2202 | + '<input type="checkbox" name="' . esc_attr(Metasync_Admin::option_key) . '[common_meta_settings][twitter_image_alt]" value="true" %s />', | |
| 2350 | 2203 | $val === 'true' ? 'checked' : '' |
| 2351 | 2204 | ); |
| 2352 | 2205 | printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>'); |
| 2353 | 2206 | } |
| @@ -2354,9 +2207,9 @@ | ||
| 2354 | 2207 | |
| 2355 | 2208 | public function facebook_page_url_callback() |
| 2356 | 2209 | { |
| 2357 | 2210 | $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? ''; |
| 2358 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url)); | |
| 2211 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url)); | |
| 2359 | 2212 | printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>'); |
| 2360 | 2213 | } |
| 2361 | 2214 | |
| 2362 | 2215 | public function facebook_authorship_callback() |
| @@ -2361,9 +2214,9 @@ | ||
| 2361 | 2214 | |
| 2362 | 2215 | public function facebook_authorship_callback() |
| 2363 | 2216 | { |
| 2364 | 2217 | $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? ''; |
| 2365 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship)); | |
| 2218 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship)); | |
| 2366 | 2219 | printf('<br><span class="description"> Enter Facebook profile URL to show Facebook Authorship when your articles are being shared on Facebook. eg: <code>https://www.facebook.com/shahrukh/</code> </span>'); |
| 2367 | 2220 | } |
| 2368 | 2221 | |
| 2369 | 2222 | public function facebook_admin_callback() |
| @@ -2368,9 +2221,9 @@ | ||
| 2368 | 2221 | |
| 2369 | 2222 | public function facebook_admin_callback() |
| 2370 | 2223 | { |
| 2371 | 2224 | $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? ''; |
| 2372 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin)); | |
| 2225 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin)); | |
| 2373 | 2226 | printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>'); |
| 2374 | 2227 | } |
| 2375 | 2228 | |
| 2376 | 2229 | public function facebook_app_callback() |
| @@ -2375,9 +2228,9 @@ | ||
| 2375 | 2228 | |
| 2376 | 2229 | public function facebook_app_callback() |
| 2377 | 2230 | { |
| 2378 | 2231 | $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? ''; |
| 2379 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app)); | |
| 2232 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app)); | |
| 2380 | 2233 | printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>'); |
| 2381 | 2234 | } |
| 2382 | 2235 | |
| 2383 | 2236 | public function facebook_secret_callback() |
| @@ -2382,9 +2235,9 @@ | ||
| 2382 | 2235 | |
| 2383 | 2236 | public function facebook_secret_callback() |
| 2384 | 2237 | { |
| 2385 | 2238 | $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? ''; |
| 2386 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret)); | |
| 2239 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret)); | |
| 2387 | 2240 | printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>'); |
| 2388 | 2241 | } |
| 2389 | 2242 | |
| 2390 | 2243 | public function twitter_username_callback() |
| @@ -2389,9 +2242,9 @@ | ||
| 2389 | 2242 | |
| 2390 | 2243 | public function twitter_username_callback() |
| 2391 | 2244 | { |
| 2392 | 2245 | $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? ''; |
| 2393 | - printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username)); | |
| 2246 | + printf('<input type="text" name="' . esc_attr(Metasync_Admin::option_key) . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username)); | |
| 2394 | 2247 | printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>'); |
| 2395 | 2248 | } |
| 2396 | 2249 | |
| 2397 | 2250 | public static function get_business_types() |
| @@ -2566,8 +2419,14 @@ | ||
| 2566 | 2419 | } |
| 2567 | 2420 | |
| 2568 | 2421 | /** |
| 2569 | 2422 | * Render breadcrumbs settings section |
| 2423 | + * | |
| 2424 | + * Rendered by the dedicated Breadcrumbs admin page. The three checkbox | |
| 2425 | + * fields each ship a hidden companion input so their keys are always | |
| 2426 | + * present in the submitted payload, which lets the save handlers treat an | |
| 2427 | + * absent key as "this form does not manage that field" instead of | |
| 2428 | + * "unchecked". | |
| 2570 | 2429 | */ |
| 2571 | 2430 | public function render_breadcrumbs_section() { |
| 2572 | 2431 | $options = Metasync::get_option('breadcrumbs', array()); |
| 2573 | 2432 | |
| @@ -2572,17 +2431,29 @@ | ||
| 2572 | 2431 | $options = Metasync::get_option('breadcrumbs', array()); |
| 2573 | 2432 | |
| 2574 | 2433 | $defaults = array( |
| 2575 | 2434 | 'enabled' => true, |
| 2576 | - 'separator' => '»', | |
| 2435 | + 'separator' => '»', | |
| 2577 | 2436 | 'home_label' => 'Home', |
| 2578 | 2437 | 'home_url' => '', |
| 2579 | 2438 | 'show_current_page' => true, |
| 2580 | 2439 | 'prefix_text' => '', |
| 2581 | 2440 | 'archive_label_format' => '{name}', |
| 2441 | + 'disable_schema' => false, | |
| 2582 | 2442 | ); |
| 2583 | 2443 | |
| 2584 | 2444 | $options = wp_parse_args($options, $defaults); |
| 2445 | + | |
| 2446 | + # Older builds stored the separator as an HTML entity (e.g. "»"). | |
| 2447 | + # Decode before matching so the saved choice still lights up its radio. | |
| 2448 | + $separator_presets = array('»', '/', '>', '·'); | |
| 2449 | + $separator_current = html_entity_decode((string) $options['separator'], ENT_QUOTES, 'UTF-8'); | |
| 2450 | + if ($separator_current === '') { | |
| 2451 | + $separator_current = '»'; | |
| 2452 | + } | |
| 2453 | + # A separator that predates the radio picker (or came from the old free | |
| 2454 | + # text field) gets its own option so it stays selected and round-trips. | |
| 2455 | + $separator_custom = in_array($separator_current, $separator_presets, true) ? '' : $separator_current; | |
| 2585 | 2456 | ?> |
| 2586 | 2457 | <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;"> |
| 2587 | 2458 | |
| 2588 | 2459 | <!-- Enable/Disable --> |
| @@ -2587,8 +2458,9 @@ | ||
| 2587 | 2458 | |
| 2588 | 2459 | <!-- Enable/Disable --> |
| 2589 | 2460 | <div style="margin-bottom: 24px;"> |
| 2590 | 2461 | <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;"> |
| 2462 | + <input type="hidden" name="metasync_options[breadcrumbs][enabled]" value="0"> | |
| 2591 | 2463 | <input type="checkbox" |
| 2592 | 2464 | name="metasync_options[breadcrumbs][enabled]" |
| 2593 | 2465 | value="1" |
| 2594 | 2466 | <?php checked($options['enabled'], 1); ?> |
| @@ -2593,8 +2465,9 @@ | ||
| 2593 | 2465 | value="1" |
| 2594 | 2466 | <?php checked($options['enabled'], 1); ?> |
| 2595 | 2467 | style="width: 18px; height: 18px; cursor: pointer;"> |
| 2596 | 2468 | <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span> |
| 2469 | + <?php Metasync::render_tooltip_icon('breadcrumbs_enabled', 'Shows a trail of links (e.g. Home > Blog > This Post) near the top of your pages so visitors can see where they are and navigate back up a level. Turn off to hide breadcrumbs everywhere.'); ?> | |
| 2597 | 2470 | </label> |
| 2598 | 2471 | <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);"> |
| 2599 | 2472 | Enable or disable breadcrumb navigation on your site |
| 2600 | 2473 | </p> |
| @@ -2599,8 +2472,25 @@ | ||
| 2599 | 2472 | Enable or disable breadcrumb navigation on your site |
| 2600 | 2473 | </p> |
| 2601 | 2474 | </div> |
| 2602 | 2475 | |
| 2476 | + <!-- Disable Schema Markup --> | |
| 2477 | + <div style="margin-bottom: 24px;"> | |
| 2478 | + <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;"> | |
| 2479 | + <input type="hidden" name="metasync_options[breadcrumbs][disable_schema]" value="0"> | |
| 2480 | + <input type="checkbox" | |
| 2481 | + name="metasync_options[breadcrumbs][disable_schema]" | |
| 2482 | + value="1" | |
| 2483 | + <?php checked($options['disable_schema'], 1); ?> | |
| 2484 | + style="width: 18px; height: 18px; cursor: pointer;"> | |
| 2485 | + <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span> | |
| 2486 | + <?php Metasync::render_tooltip_icon('breadcrumbs_disable_schema', 'Leave off. Only check this if another SEO plugin (Yoast, Rank Math, etc.) already sends breadcrumb data to Google, so you don\'t send it twice.'); ?> | |
| 2487 | + </label> | |
| 2488 | + <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);"> | |
| 2489 | + When checked, MetaSync will not output the <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">BreadcrumbList</code> JSON-LD in <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;"><head></code>. Use this if another SEO plugin already handles breadcrumb schema. | |
| 2490 | + </p> | |
| 2491 | + </div> | |
| 2492 | + | |
| 2603 | 2493 | <!-- Separator Character --> |
| 2604 | 2494 | <div style="margin-bottom: 24px;"> |
| 2605 | 2495 | <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 2606 | 2496 | Separator Character |
| @@ -2605,24 +2495,21 @@ | ||
| 2605 | 2495 | <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 2606 | 2496 | Separator Character |
| 2607 | 2497 | </label> |
| 2608 | 2498 | <div style="display: flex; gap: 12px; flex-wrap: wrap;"> |
| 2499 | + <?php foreach ($separator_presets as $separator_option) : ?> | |
| 2609 | 2500 | <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> |
| 2610 | - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>> | |
| 2611 | - <span style="color: var(--dashboard-text);">»</span> | |
| 2501 | + <input type="radio" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_option); ?>" <?php checked($separator_current, $separator_option); ?>> | |
| 2502 | + <span style="color: var(--dashboard-text);"><?php echo esc_html($separator_option); ?></span> | |
| 2612 | 2503 | </label> |
| 2613 | - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> | |
| 2614 | - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>> | |
| 2615 | - <span style="color: var(--dashboard-text);">/</span> | |
| 2616 | - </label> | |
| 2617 | - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> | |
| 2618 | - <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>> | |
| 2619 | - <span style="color: var(--dashboard-text);">></span> | |
| 2620 | - </label> | |
| 2621 | - <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> | |
| 2622 | - <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>> | |
| 2623 | - <span style="color: var(--dashboard-text);">·</span> | |
| 2624 | - </label> | |
| 2504 | + <?php endforeach; ?> | |
| 2505 | + <div style="display: flex; align-items: center; gap: 8px;"> | |
| 2506 | + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> | |
| 2507 | + <input type="radio" id="metasync-breadcrumb-separator-custom" name="metasync_options[breadcrumbs][separator]" value="<?php echo esc_attr($separator_custom); ?>" <?php checked($separator_custom !== ''); ?>> | |
| 2508 | + <span style="color: var(--dashboard-text);">Custom</span> | |
| 2509 | + </label> | |
| 2510 | + <input type="text" value="<?php echo esc_attr($separator_custom); ?>" placeholder="~" maxlength="16" aria-label="Custom separator character" oninput="var r=document.getElementById('metasync-breadcrumb-separator-custom');r.value=this.value;r.checked=true" style="width: 80px; padding: 8px 10px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px;"> | |
| 2511 | + </div> | |
| 2625 | 2512 | </div> |
| 2626 | 2513 | <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);"> |
| 2627 | 2514 | Choose how to separate breadcrumb items |
| 2628 | 2515 | </p> |
| @@ -2662,8 +2549,9 @@ | ||
| 2662 | 2549 | |
| 2663 | 2550 | <!-- Show Current Page --> |
| 2664 | 2551 | <div style="margin-bottom: 24px;"> |
| 2665 | 2552 | <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;"> |
| 2553 | + <input type="hidden" name="metasync_options[breadcrumbs][show_current_page]" value="0"> | |
| 2666 | 2554 | <input type="checkbox" |
| 2667 | 2555 | name="metasync_options[breadcrumbs][show_current_page]" |
| 2668 | 2556 | value="1" |
| 2669 | 2557 | <?php checked($options['show_current_page'], 1); ?> |
| @@ -2694,8 +2582,9 @@ | ||
| 2694 | 2582 | <!-- Archive Label Format --> |
| 2695 | 2583 | <div style="margin-bottom: 24px;"> |
| 2696 | 2584 | <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);"> |
| 2697 | 2585 | Archive Label Format |
| 2586 | + <?php Metasync::render_tooltip_icon('breadcrumb_archive_format', 'On category/archive pages, {name} is replaced with that section\'s name. Type {name} alone for just the name, or something like "Category: {name}".'); ?> | |
| 2698 | 2587 | </label> |
| 2699 | 2588 | <input type="text" |
| 2700 | 2589 | id="breadcrumb_archive_format" |
| 2701 | 2590 | name="metasync_options[breadcrumbs][archive_label_format]" |