PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.17
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.17
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
metasync / includes / class-metasync-settings-fields.php

class-metasync-settings-fields.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.17, at includes/class-metasync-settings-fields.php

2,549 lines 133.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Settings field callbacks and accordion/section helpers extracted from Metasync_Admin.
9 *
10 * @package Metasync
11 * @subpackage Metasync/includes
12 */
13 class Metasync_Settings_Fields {
14
15 private static $instance = null;
16
17 /**
18 * Reference to the admin instance for callbacks to non-extracted admin methods.
19 *
20 * @var Metasync_Admin|null
21 */
22 private $admin_instance = null;
23
24 private function __construct() {}
25
26 /**
27 * @return self
28 */
29 public static function instance() {
30 if (null === self::$instance) {
31 self::$instance = new self();
32 }
33 return self::$instance;
34 }
35
36 /**
37 * Inject the admin instance so callbacks to non-extracted methods still work.
38 *
39 * @param Metasync_Admin $admin
40 */
41 public function set_admin_instance($admin) {
42 $this->admin_instance = $admin;
43 }
44
45 // ────────────────────────────────────────────────────────────────
46 // Accordion / Section helpers
47 // ────────────────────────────────────────────────────────────────
48
49 public function get_accordion_sections_config() {
50 return array(
51 'connection' => array(
52 'title' => 'Connection & Authentication',
53 'description' => 'Manage your API connection and authentication settings',
54 'icon' => 'admin-network',
55 'priority' => 10,
56 'default_open' => true,
57 'fields' => array(
58 'searchatlas_api_key',
59 'apikey'
60 )
61 ),
62 'otto_ssr' => array(
63 'title' => Metasync::get_whitelabel_otto_name() . ' Server-Side Rendering',
64 'description' => 'Configure ' . Metasync::get_whitelabel_otto_name() . ' rendering and display options',
65 'icon' => 'performance',
66 'priority' => 20,
67 'default_open' => false,
68 'fields' => array(
69 'otto_pixel_uuid',
70 'otto_disable_on_loggedin',
71 'otto_disable_preview_button',
72 'otto_wp_rocket_compat',
73 )
74 ),
75 'edge_cache' => array(
76 'title' => 'Edge Cache / CDN',
77 'description' => 'Configure CDN credentials for cache-tag purging',
78 'icon' => 'admin-site-alt3',
79 'priority' => 22,
80 'default_open' => false,
81 'render_callback' => array('Metasync_Edge_Cache_Settings', 'render'),
82 ),
83 'bot_detection' => array(
84 'title' => 'Bot Detection & Filtering',
85 'description' => 'Manage bot traffic and reduce unnecessary API calls',
86 'icon' => 'filter',
87 'priority' => 25,
88 'default_open' => false,
89 'fields' => array(
90 'otto_disable_for_bots',
91 'otto_bot_whitelist',
92 'otto_bot_blacklist',
93 'otto_bot_statistics_link'
94 )
95 ),
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 'open_graph' => array(
105 'title' => 'Open Graph',
106 'description' => 'Control which Open Graph and article meta tags are output',
107 'icon' => 'share',
108 'priority' => 38,
109 'default_open' => false,
110 'fields' => array(
111 'og_image_dimensions',
112 'article_timestamps',
113 'article_author',
114 'article_section',
115 'article_tags',
116 'twitter_image_alt'
117 )
118 ),
119 'editor_settings' => array(
120 'title' => 'Post/Page Editor Settings',
121 'description' => 'Customize meta boxes and editor functionality',
122 'icon' => 'edit',
123 'priority' => 40,
124 'default_open' => false,
125 'fields' => array(
126 'disable_common_robots_metabox',
127 'disable_advance_robots_metabox',
128 'disable_redirection_metabox',
129 'disable_canonical_metabox',
130 'disable_social_opengraph_metabox',
131 'disable_schema_markup_metabox',
132 'disable_seo_metabox',
133 'open_external_links'
134 )
135 ),
136 'user_management' => array(
137 'title' => 'User Management for Content',
138 'description' => 'Configure which users are allowed to be authors of content synced.',
139 'icon' => 'groups',
140 'priority' => 50,
141 'default_open' => false,
142 'fields' => array(
143 'content_genius_sync_roles'
144 )
145 ),
146 'content_rendering' => array(
147 'title' => 'Content Rendering',
148 'description' => 'Configure how synced content is stored and rendered by your page builder',
149 'icon' => 'admin-appearance',
150 'priority' => 55,
151 'default_open' => false,
152 'fields' => array(
153 'default_page_builder'
154 )
155 ),
156 'llms_txt' => array(
157 'title' => 'LLMs.txt',
158 'description' => 'Publish a standards-compliant /llms.txt so AI search engines and assistants can discover your content',
159 'icon' => 'media-text',
160 'priority' => 56,
161 'default_open' => false,
162 'render_callback' => array($this, 'render_llms_txt_section'),
163 ),
164 'advanced' => array(
165 'title' => 'Plugin Settings',
166 'description' => 'System configuration and maintenance options',
167 'icon' => 'admin-settings',
168 'priority' => 60,
169 'default_open' => false,
170 'fields' => array(
171 'permalink_structure',
172 'hide_dashboard_framework',
173 'show_admin_bar_status',
174 'enable_auto_updates',
175 'import_external_data'
176 )
177 )
178 );
179 }
180
181 public function get_advanced_accordion_config() {
182 $config = array();
183
184 $user = wp_get_current_user();
185 if (in_array('administrator', (array) $user->roles)) {
186 $config['plugin_access'] = array(
187 'title' => 'User Roles with Plugin Access',
188 'description' => 'Control which user roles can see and access this plugin',
189 'icon' => 'admin-users',
190 'priority' => 5,
191 'default_open' => false,
192 'render_callback' => array($this, 'render_plugin_access_roles_section')
193 );
194 }
195
196 $config['debug_mode'] = array(
197 'title' => 'Debug Mode',
198 'description' => 'Manage debug mode with automatic disable and safety limits',
199 'icon' => 'info',
200 'priority' => 8,
201 'default_open' => false,
202 'render_callback' => array($this->admin_instance, 'render_debug_mode_section')
203 );
204
205 $config['error_logs'] = array(
206 'title' => 'Error Logs',
207 'description' => 'View and manage error logs to troubleshoot issues',
208 'icon' => 'warning',
209 'priority' => 10,
210 'default_open' => true,
211 'render_callback' => array($this->admin_instance, 'render_error_log_content')
212 );
213
214 $config['execution_settings'] = array(
215 'title' => 'Execution Settings',
216 'description' => 'Configure resource limits and execution parameters',
217 'icon' => 'controls-play',
218 'priority' => 15,
219 'default_open' => false,
220 'render_callback' => array($this, 'render_execution_settings_section')
221 );
222
223 $config['otto_cache'] = array(
224 'title' => 'Cache Management',
225 'description' => 'Manage ' . Metasync::get_whitelabel_otto_name() . ' cache and clear all cache plugins',
226 'icon' => 'database',
227 'priority' => 20,
228 'default_open' => false,
229 'render_callback' => array($this->admin_instance, 'render_otto_cache_management')
230 );
231
232 $config['db_cleanup'] = array(
233 'title' => 'Database Cleanup',
234 'description' => 'Remove orphaned data and schedule automated weekly cleanup',
235 'icon' => 'trash',
236 'priority' => 25,
237 'default_open' => false,
238 'render_callback' => array($this->admin_instance, 'render_db_cleanup_section')
239 );
240
241 $config['performance'] = array(
242 'title' => 'Performance & CPU Load',
243 'description' => 'Configure CPU load thresholds to prevent processing during peak server load',
244 'icon' => 'chart-line',
245 'priority' => 27,
246 'default_open' => false,
247 'render_callback' => array($this->admin_instance, 'render_cpu_monitor_section')
248 );
249
250 $config['reset_settings'] = array(
251 'title' => 'Reset Plugin Settings',
252 'description' => 'Reset all plugin settings to default values',
253 'icon' => 'update',
254 'priority' => 30,
255 'default_open' => false,
256 'render_callback' => array($this, 'render_reset_settings_section')
257 );
258
259 return $config;
260 }
261
262 public function render_advanced_accordion() {
263 $sections_config = $this->get_advanced_accordion_config();
264
265 uasort($sections_config, function($a, $b) {
266 return $a['priority'] - $b['priority'];
267 });
268
269 echo '<div class="metasync-settings-accordion">';
270
271 foreach ($sections_config as $section_key => $section_data) {
272 $section_id = 'metasync-advanced-section-' . $section_key;
273 $is_open = $section_data['default_open'];
274 $aria_expanded = $is_open ? 'true' : 'false';
275 $content_state = $is_open ? 'open' : 'closed';
276
277 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
278
279 echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
280 echo '<div class="metasync-accordion-title">';
281 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
282 echo '<div class="metasync-accordion-text">';
283 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
284 echo '<p class="metasync-accordion-description">' . esc_html($section_data['description']) . '</p>';
285 echo '</div>';
286 echo '</div>';
287 echo '<button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">';
288 echo '<span class="toggle-icon">▼</span>';
289 echo '</button>';
290 echo '</div>';
291
292 echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
293
294 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
295 call_user_func($section_data['render_callback']);
296 }
297
298 echo '</div>';
299 echo '</div>';
300 }
301
302 echo '</div>';
303 }
304
305 public function render_reset_settings_section() {
306 ?>
307 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
308 <div style="background: rgba(255, 243, 205, 0.1); border: 1px solid rgba(255, 234, 167, 0.3); border-radius: 8px; padding: 20px; margin-bottom: 20px;">
309 <h4 style="color: var(--dashboard-warning, #f59e0b); margin: 0 0 12px 0; display: flex; align-items: center; gap: 8px;">
310 <span class="dashicons dashicons-warning" style="color:var(--dashboard-warning,#f59e0b);font-size:18px;width:18px;height:18px;"></span>
311 <span>Important Warning</span>
312 </h4>
313 <p style="color: var(--dashboard-text-secondary); margin: 0 0 12px 0;">This action will permanently delete:</p>
314 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 12px 20px; line-height: 1.8;">
315 <li>All API keys and authentication tokens</li>
316 <li>White label branding settings</li>
317 <li>Plugin configuration and preferences</li>
318 <li>Instant indexing settings</li>
319 <li>Media optimization settings and cache</li>
320 <li>Code minification settings</li>
321 <li>All cached data and crawl information</li>
322 </ul>
323 <p style="color: var(--dashboard-warning, #f59e0b); margin: 0; font-weight: 600;">You will need to reconfigure the plugin completely after this reset.</p>
324 </div>
325 <form method="post" action="" onsubmit="return confirmClearSettings(event)">
326 <?php wp_nonce_field('metasync_clear_all_settings_nonce', 'clear_all_settings_nonce'); ?>
327 <input type="hidden" name="clear_all_settings" value="yes" />
328 <button type="submit" class="metasync-btn-danger" style="background: var(--dashboard-error, #ef4444); color: #ffffff; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);" onmouseover="this.style.background='#dc2626'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(239, 68, 68, 0.3)';" onmouseout="this.style.background='var(--dashboard-error, #ef4444)'; this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(239, 68, 68, 0.2)';">
329 <span class="dashicons dashicons-trash" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Clear All Settings
330 </button>
331 </form>
332 </div>
333 <script>
334 function confirmClearSettings(event) {
335 event.preventDefault();
336
337 var firstConfirm = confirm("⚠️ WARNING: This will permanently delete ALL plugin settings!\n\nThis action cannot be undone. Are you sure you want to continue?");
338 if (!firstConfirm) {
339 return false;
340 }
341
342 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• Media optimization settings and cache\n• Code minification 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.");
343 if (!secondConfirm) {
344 return false;
345 }
346
347 var typeConfirm = prompt("Type 'DELETE' (in capital letters) to confirm you want to permanently clear all settings:");
348 if (typeConfirm !== 'DELETE') {
349 alert("Settings reset cancelled. Type 'DELETE' exactly to confirm.");
350 return false;
351 }
352
353 event.target.submit();
354 return false;
355 }
356 </script>
357 <?php
358 }
359
360 public function render_google_index_section() {
361 if (!function_exists('google_index_direct')) {
362 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
363 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
364 } else {
365 error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
366 return;
367 }
368 }
369
370 $google_index = google_index_direct();
371 $service_info = $google_index->get_service_account_info();
372 $is_configured = !isset($service_info['error']);
373
374 $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
375
376 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
377
378 // Render post types selection
379 $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
380 $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
381 include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php';
382 }
383
384 public function render_bing_index_section() {
385 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
386 $bing_index = new Metasync_Bing_Instant_Index();
387 $api_key = $bing_index->get_setting('api_key');
388 $endpoint = $bing_index->get_setting('endpoint', 'indexnow');
389 $post_types_settings = $bing_index->get_setting('post_types', []);
390 $is_configured = !empty($api_key);
391
392 $post_types = get_post_types(['public' => true], 'objects');
393
394 ?>
395 <div style="padding: 20px;">
396 <!-- About IndexNow (Collapsible) -->
397 <details style="margin-bottom: 20px;">
398 <summary style="cursor: pointer; font-weight: 600; color: var(--dashboard-text-primary);">
399 About IndexNow Protocol
400 </summary>
401 <div style="margin-top: 10px; padding: 15px; background: rgba(255, 255, 255, 0.03); border-radius: 4px;">
402 <p style="color: var(--dashboard-text-secondary); margin: 0 0 10px 0;">
403 IndexNow is a simple protocol that allows websites to instantly notify search engines about URL changes.
404 It's supported by Bing, Yandex, Naver, Seznam, and other search engines.
405 </p>
406 <ul style="color: var(--dashboard-text-secondary); margin: 0 0 10px 20px;">
407 <li>✓ Instant notification to multiple search engines</li>
408 <li>✓ Simple API key authentication (no OAuth required)</li>
409 <li>✓ Supports batch URL submissions (up to 10,000 URLs)</li>
410 <li>✓ Free to use with no quotas</li>
411 <li>✓ Fire-and-forget protocol (no status checking needed)</li>
412 </ul>
413 <p style="color: var(--dashboard-text-secondary); margin: 0;">
414 <strong>Resources:</strong>
415 <a href="https://www.indexnow.org/documentation" target="_blank" style="color: var(--dashboard-accent);">IndexNow Documentation</a> |
416 <a href="https://www.bing.com/webmasters" target="_blank" style="color: var(--dashboard-accent);">Bing Webmaster Tools</a>
417 </p>
418 </div>
419 </details>
420
421 <!-- Configuration Status -->
422 <div style="margin-bottom: 20px;">
423 <?php if ($is_configured): ?>
424 <div style="padding: 12px; background: rgba(76, 175, 80, 0.1); border-left: 4px solid #4caf50; border-radius: 4px;">
425 <p style="margin: 0; color: var(--dashboard-text-primary);">
426 <strong style="color: #4caf50;">IndexNow API Configured</strong><br>
427 <span style="color: var(--dashboard-text-secondary); font-size: 0.95em;">
428 API Key: <code style="padding: 2px 6px; background: rgba(0,0,0,0.1); border-radius: 3px;"><?php echo esc_html(substr($api_key, 0, 8) . '...' . substr($api_key, -8)); ?></code>
429 </span>
430 </p>
431 </div>
432 <?php else: ?>
433 <div style="padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 4px solid #ff9800; border-radius: 4px;">
434 <p style="margin: 0; color: var(--dashboard-text-primary);">
435 <strong style="color: #ff9800;">Configuration Required</strong><br>
436 <span style="color: var(--dashboard-text-secondary); font-size: 0.95em;">
437 Configure your IndexNow API key below to enable instant indexing with Bing and other search engines.
438 </span>
439 </p>
440 </div>
441 <?php endif; ?>
442 </div>
443
444 <!-- Settings Fields -->
445 <table class="form-table" style="margin-top: 0;">
446 <tr>
447 <th scope="row" style="width: 200px; padding-top: 15px;">
448 <label for="metasync_bing_api_key_inline">IndexNow API Key <span style="color: #d63638;">*</span></label>
449 </th>
450 <td style="padding-top: 15px;">
451 <input type="text"
452 name="metasync_bing_api_key_inline"
453 id="metasync_bing_api_key_inline"
454 class="large-text"
455 value="<?php echo esc_attr($api_key); ?>"
456 placeholder="Enter your IndexNow API key (32+ character hexadecimal string)" />
457 <br>
458 <button type="button"
459 id="generate-bing-api-key-inline"
460 class="button button-secondary"
461 style="margin-top: 8px;">
462 Generate Random API Key
463 </button>
464 <p class="description" style="margin-top: 8px;">
465 Your IndexNow API key is required to submit URLs for instant indexing. You can generate a random key or use your own (32+ character hexadecimal string).
466 After saving, a verification file will be automatically created at your site root.
467 </p>
468 </td>
469 </tr>
470
471 <tr>
472 <th scope="row" style="padding-top: 15px;">
473 <label>API Endpoint</label>
474 </th>
475 <td style="padding-top: 15px;">
476 <fieldset>
477 <label style="display: block; margin-bottom: 8px;">
478 <input type="radio"
479 name="metasync_bing_endpoint_inline"
480 value="indexnow"
481 <?php checked($endpoint, 'indexnow'); ?>>
482 <strong>IndexNow.org</strong> (Recommended)
483 <span style="color: var(--dashboard-text-secondary); font-size: 0.9em; display: block; margin-left: 24px;">
484 Notifies Bing, Yandex, Naver, Seznam, and other participating search engines
485 </span>
486 </label>
487 <label style="display: block;">
488 <input type="radio"
489 name="metasync_bing_endpoint_inline"
490 value="bing"
491 <?php checked($endpoint, 'bing'); ?>>
492 <strong>Bing.com</strong> (Bing-specific)
493 <span style="color: var(--dashboard-text-secondary); font-size: 0.9em; display: block; margin-left: 24px;">
494 Direct submission to Bing only
495 </span>
496 </label>
497 </fieldset>
498 <p class="description">
499 Select which endpoint to use for submitting URLs. The IndexNow.org endpoint is recommended as it notifies multiple search engines at once.
500 </p>
501 </td>
502 </tr>
503
504 <tr>
505 <th scope="row" style="padding-top: 15px;">
506 <label>Auto-Submit Post Types</label>
507 </th>
508 <td style="padding-top: 15px;">
509 <fieldset>
510 <?php foreach ($post_types as $post_type): ?>
511 <label style="display: inline-block; margin-right: 20px; margin-bottom: 8px;">
512 <input type="checkbox"
513 name="metasync_bing_post_types_inline[<?php echo esc_attr($post_type->name); ?>]"
514 value="<?php echo esc_attr($post_type->name); ?>"
515 <?php checked(in_array($post_type->name, $post_types_settings, true)); ?>>
516 <?php echo esc_html($post_type->label); ?>
517 </label>
518 <?php endforeach; ?>
519 </fieldset>
520 <p class="description">
521 Selected post types will be automatically submitted to IndexNow when published or updated.
522 </p>
523 </td>
524 </tr>
525
526 <tr>
527 <th scope="row" style="padding-top: 15px;">
528 <label>Plugin Source Control</label>
529 </th>
530 <td style="padding-top: 15px;">
531 <fieldset>
532 <label style="display: flex; align-items: flex-start; gap: 8px;">
533 <input type="checkbox"
534 name="metasync_bing_disable_other_plugins_inline"
535 value="1"
536 <?php checked($bing_index->get_setting('disable_other_plugins', true), true); ?>
537 style="margin-top: 2px;">
538 <span>
539 <strong>Disable other IndexNow plugins</strong>
540 <span style="display: block; color: var(--dashboard-text-secondary); font-size: 0.9em; margin-top: 4px; font-weight: normal;">
541 When enabled, Our plugin will be the exclusive source for IndexNow submissions in Bing Webmaster Tools.
542 This disables IndexNow features in Yoast SEO, Rank Math, and other competing plugins to prevent duplicate submissions.
543 </span>
544 </span>
545 </label>
546 </fieldset>
547 </td>
548 </tr>
549 </table>
550
551 </div>
552
553 <script>
554 jQuery(document).ready(function($) {
555 $('#generate-bing-api-key-inline').on('click', function() {
556 const array = new Uint8Array(16);
557 crypto.getRandomValues(array);
558 const hexString = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
559 $('#metasync_bing_api_key_inline').val(hexString);
560
561 $('#enable_binginstantindex').prop('checked', true);
562
563 $('#metasync_bing_api_key_inline').trigger('change');
564 });
565
566 $('#metasync_bing_api_key_inline').on('input', function() {
567 const apiKey = $(this).val().trim();
568 if (apiKey.length >= 8) {
569 $('#enable_binginstantindex').prop('checked', true);
570 }
571 });
572
573 function toggleBingConfig() {
574 const isEnabled = $('#enable_binginstantindex').is(':checked');
575 const $apiConfig = $('#enable_binginstantindex').closest('tr').nextAll('tr').first();
576 if (isEnabled) {
577 $apiConfig.show();
578 } else {
579 $apiConfig.hide();
580 }
581 }
582
583 toggleBingConfig();
584
585 $('#enable_binginstantindex').on('change', toggleBingConfig);
586 });
587 </script>
588
589 <style>
590 .form-table th {
591 color: var(--dashboard-text-primary);
592 }
593
594 .form-table td {
595 color: var(--dashboard-text-secondary);
596 }
597
598 .form-table code {
599 padding: 2px 6px;
600 background: rgba(0, 0, 0, 0.1);
601 border-radius: 3px;
602 font-size: 0.9em;
603 }
604
605 details summary {
606 transition: color 0.2s;
607 }
608
609 details summary:hover {
610 color: var(--dashboard-accent);
611 }
612
613 details[open] summary {
614 margin-bottom: 10px;
615 }
616 </style>
617 <?php
618 }
619
620 public function render_plugin_access_roles_section() {
621 $general_options = Metasync::get_option('general');
622
623 $setting_exists = isset($general_options['plugin_access_roles']);
624 $selected_roles = $general_options['plugin_access_roles'] ?? null;
625
626 if (is_string($selected_roles)) {
627 $selected_roles = array($selected_roles);
628 } elseif (!is_array($selected_roles)) {
629 $selected_roles = null;
630 }
631
632 if (!function_exists('wp_roles')) {
633 require_once(ABSPATH . 'wp-admin/includes/user.php');
634 }
635 global $wp_roles;
636
637 if (!isset($wp_roles) || !is_object($wp_roles)) {
638 $wp_roles = new WP_Roles();
639 }
640
641 $all_roles = $wp_roles->roles;
642
643 $all_roles_checked = is_array($selected_roles) && in_array('all', $selected_roles);
644 ?>
645 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
646 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
647 Select which user roles can see and access this plugin's menu, settings, and options in the WordPress admin area.
648 </p>
649
650 <form method="post" action="<?php echo admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=advanced'); ?>" id="plugin-access-roles-form">
651 <?php wp_nonce_field('metasync_plugin_access_roles_nonce', 'plugin_access_roles_nonce'); ?>
652 <input type="hidden" name="save_plugin_access_roles" value="yes" />
653
654 <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;">
655
656 <!-- All Roles Option -->
657 <label class="metasync-role-option-all" style="display: flex; align-items: center; gap: 10px; padding: 12px; cursor: pointer; border-radius: 6px; transition: background 0.2s ease;">
658 <input type="checkbox"
659 id="plugin-access-all-roles"
660 name="plugin_access_roles[]"
661 value="all"
662 style="width: 18px; height: 18px; cursor: pointer;"
663 <?php checked($all_roles_checked, true); ?> />
664 <strong style="color: var(--dashboard-text-primary);">All Roles</strong>
665 </label>
666
667 <!-- Divider -->
668 <hr style="border: none; border-top: 1px solid var(--dashboard-border); margin: 12px 0;">
669
670 <?php
671 if (!empty($all_roles) && is_array($all_roles)) {
672 foreach ($all_roles as $role_key => $role_details) {
673 if ($role_key === 'administrator') {
674 continue;
675 }
676
677 if (!isset($role_details['name'])) {
678 continue;
679 }
680
681 $is_checked = is_array($selected_roles) && in_array($role_key, $selected_roles);
682 $role_name = translate_user_role($role_details['name']);
683 ?>
684 <label class="metasync-role-option" style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer; border-radius: 6px; transition: background 0.2s ease;">
685 <input type="checkbox"
686 class="plugin-access-individual-role"
687 name="plugin_access_roles[]"
688 value="<?php echo esc_attr($role_key); ?>"
689 style="width: 18px; height: 18px; cursor: pointer;"
690 <?php checked($is_checked, true); ?> />
691 <span style="color: var(--dashboard-text-primary);"><?php echo esc_html($role_name); ?></span>
692 </label>
693 <?php
694 }
695 }
696 ?>
697
698 </div>
699
700 <!-- Description -->
701 <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px; padding: 16px; margin: 20px 0;">
702 <p style="color: var(--dashboard-text-secondary); margin: 0; font-size: 13px;">
703 <strong style="color: var(--dashboard-info, #3b82f6);">How it works:</strong><br>
704 <strong>Administrators always have access</strong> to this plugin regardless of settings.<br>
705 <strong>By default</strong>, only Administrators can access the plugin (no roles selected).<br>
706 If <strong>"All Roles"</strong> is selected, all users will see the plugin.<br>
707 Users with unchecked roles will not see the plugin in their WordPress admin area.
708 </p>
709 </div>
710
711 <button type="submit" class="metasync-btn-primary" style="background: var(--dashboard-gradient-primary); color: #ffffff; border: none; padding: 12px 24px; border-radius: 8px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 8px rgba(0, 0, 0, 0.15)';" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 4px rgba(0, 0, 0, 0.1)';">
712 Save Access Settings
713 </button>
714 </form>
715 </div>
716 <script>
717 jQuery(document).ready(function($) {
718 var $allRolesCheckbox = $('#plugin-access-all-roles');
719 var $individualRoles = $('.plugin-access-individual-role');
720
721 $allRolesCheckbox.on('change', function() {
722 if ($(this).is(':checked')) {
723 $individualRoles.prop('checked', false);
724 }
725 });
726
727 $individualRoles.on('change', function() {
728 if ($(this).is(':checked')) {
729 $allRolesCheckbox.prop('checked', false);
730 }
731 });
732 });
733 </script>
734 <?php
735 }
736
737 public function get_default_execution_settings() {
738 return array(
739 'max_execution_time' => 30,
740 'max_memory_limit' => 256,
741 'log_batch_size' => 1000,
742 'action_scheduler_batches' => 1,
743 'otto_rate_limit' => 10,
744 'otto_cache_ttl' => 30,
745 'queue_cleanup_days' => 31
746 );
747 }
748
749 public function get_execution_setting($key, $default = null) {
750 $settings = get_option('metasync_execution_settings', array());
751 $defaults = $this->get_default_execution_settings();
752
753 if (empty($settings)) {
754 return isset($defaults[$key]) ? $defaults[$key] : $default;
755 }
756
757 return isset($settings[$key]) ? $settings[$key] : (isset($defaults[$key]) ? $defaults[$key] : $default);
758 }
759
760 public function get_all_execution_settings() {
761 $settings = get_option('metasync_execution_settings', array());
762 $defaults = $this->get_default_execution_settings();
763
764 return wp_parse_args($settings, $defaults);
765 }
766
767 public function can_change_memory_limit() {
768 $current_limit = ini_get('memory_limit');
769
770 $test_result = @ini_set('memory_limit', $current_limit);
771
772 return $test_result !== false;
773 }
774
775 public function get_server_limits() {
776 $max_execution_time = ini_get('max_execution_time');
777 $memory_limit = ini_get('memory_limit');
778
779 // Prefer the server-configured value: WordPress (wp_raise_memory_limit)
780 // or this plugin (apply_memory_limit) may have already changed the
781 // runtime limit via ini_set, so ini_get no longer reflects the server
782 // configuration. get_cfg_var reads the php.ini value untouched.
783 $config_memory_limit = get_cfg_var('memory_limit');
784 if ($config_memory_limit !== false && $config_memory_limit !== '') {
785 $memory_limit = $config_memory_limit;
786 }
787
788 $memory_limit_mb = $this->parse_memory_limit_to_mb($memory_limit);
789
790 $can_change_memory = $this->can_change_memory_limit();
791
792 $memory_limit_display = $memory_limit_mb == -1 ? 'Unlimited' : $memory_limit_mb . ' MB';
793
794 return array(
795 'max_execution_time' => $max_execution_time == -1 ? 'Unlimited' : $max_execution_time . ' seconds',
796 'memory_limit' => $memory_limit_display,
797 'max_execution_time_raw' => $max_execution_time,
798 'memory_limit_raw' => $memory_limit_mb,
799 'can_change_memory' => $can_change_memory
800 );
801 }
802
803 /**
804 * Allowed range for the Max Memory Limit setting: 64 MB up to the
805 * server-configured memory limit (null max when the server is unlimited).
806 */
807 public function get_memory_limit_allowed_range() {
808 $server_limits = $this->get_server_limits();
809 $max = $server_limits['memory_limit_raw'] == -1 ? null : max(64, (int) $server_limits['memory_limit_raw']);
810
811 return array(
812 'min' => 64,
813 'max' => $max
814 );
815 }
816
817 public function apply_memory_limit() {
818 if (!$this->can_change_memory_limit()) {
819 return false;
820 }
821
822 $memory_limit_mb = $this->get_execution_setting('max_memory_limit');
823
824 $result = @ini_set('memory_limit', $memory_limit_mb . 'M');
825
826 return $result !== false;
827 }
828
829 public function parse_memory_limit_to_mb($memory_limit) {
830 if ($memory_limit == -1 || $memory_limit == '-1') {
831 return -1;
832 }
833
834 $memory_limit = trim($memory_limit);
835 $last = strtolower(substr($memory_limit, -1));
836 $value = (int) $memory_limit;
837
838 switch ($last) {
839 case 'g':
840 $value *= 1024;
841 break;
842 case 'm':
843 break;
844 case 'k':
845 $value /= 1024;
846 break;
847 }
848
849 return $value;
850 }
851
852 public function render_execution_settings_section() {
853 $settings = $this->get_all_execution_settings();
854 $server_limits = $this->get_server_limits();
855 $memory_range = $this->get_memory_limit_allowed_range();
856
857 $warnings = array();
858 if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
859 $warnings['max_execution_time'] = true;
860 }
861 if ($server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
862 $warnings['max_memory_limit'] = true;
863 }
864 ?>
865 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
866 <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">
867 Configure resource limits and execution parameters for plugin operations. Adjust these settings based on your server capabilities.
868 </p>
869
870 <form id="metasync-execution-settings-form" method="post">
871 <?php wp_nonce_field('metasync_execution_settings_nonce', 'execution_settings_nonce'); ?>
872
873 <!-- Execution & Memory Section -->
874 <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;">
875 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Execution & Memory</h3>
876
877 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
878 <div>
879 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
880 Max Execution Time:
881 </label>
882 <div style="display: flex; align-items: center; gap: 10px;">
883 <input type="number"
884 id="max_execution_time"
885 name="max_execution_time"
886 value="<?php echo esc_attr($settings['max_execution_time']); ?>"
887 min="1"
888 max="300"
889 required
890 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
891 <span style="color: var(--dashboard-text-secondary);">seconds</span>
892 </div>
893 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
894 Server Limit: <?php echo esc_html($server_limits['max_execution_time']); ?>
895 </p>
896 <p id="max_execution_time_warning" style="display: none; color: #f59e0b; font-size: 12px; margin: 4px 0 0 0;">
897 <span class="dashicons dashicons-warning" style="margin-right:4px;font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
898 <span>Configured value exceeds server limit</span>
899 </p>
900 <?php if (isset($warnings['max_execution_time'])): ?>
901 <script>
902 jQuery(document).ready(function($) {
903 $('#max_execution_time_warning').show();
904 });
905 </script>
906 <?php endif; ?>
907 </div>
908
909 <div>
910 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
911 Max Memory Limit:
912 </label>
913 <div style="display: flex; align-items: center; gap: 10px;">
914 <input type="number"
915 id="max_memory_limit"
916 name="max_memory_limit"
917 value="<?php echo esc_attr($settings['max_memory_limit']); ?>"
918 min="<?php echo esc_attr($memory_range['min']); ?>"
919 <?php if ($memory_range['max'] !== null): ?>max="<?php echo esc_attr($memory_range['max']); ?>"<?php endif; ?>
920 <?php if (!$server_limits['can_change_memory']): ?>
921 readonly
922 disabled
923 <?php endif; ?>
924 required
925 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'; ?>;" />
926 <span style="color: var(--dashboard-text-secondary);">MB</span>
927 </div>
928 <p style="color: var(--dashboard-text-secondary); font-size: 12px; margin: 4px 0 0 0;">
929 Server Limit: <?php echo esc_html($server_limits['memory_limit']); ?> &middot; Allowed: <?php echo $memory_range['max'] !== null ? esc_html($memory_range['min'] . '' . $memory_range['max'] . ' MB') : esc_html($memory_range['min'] . ' MB or higher'); ?>
930 </p>
931 <?php if (!$server_limits['can_change_memory']): ?>
932 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
933 <span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
934 <span>Server does not allow changing memory limit. This setting is read-only.</span>
935 </p>
936 <?php else: ?>
937 <p id="max_memory_limit_warning" style="display: none; color: #f59e0b; font-size: 12px; margin: 4px 0 0 0;">
938 <span class="dashicons dashicons-warning" style="margin-right:4px;font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
939 <span>Configured value exceeds server limit</span>
940 </p>
941 <?php if (isset($warnings['max_memory_limit'])): ?>
942 <script>
943 jQuery(document).ready(function($) {
944 $('#max_memory_limit_warning').show();
945 });
946 </script>
947 <?php endif; ?>
948 <?php endif; ?>
949 </div>
950 </div>
951 </div>
952
953 <!-- Batch Processing Section -->
954 <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;">
955 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Batch Processing</h3>
956
957 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
958 <div>
959 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
960 Log Processing Batch Size:
961 </label>
962 <div style="display: flex; align-items: center; gap: 10px;">
963 <input type="number"
964 id="log_batch_size"
965 name="log_batch_size"
966 value="<?php echo esc_attr($settings['log_batch_size']); ?>"
967 min="100"
968 max="5000"
969 required
970 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
971 <span style="color: var(--dashboard-text-secondary);">lines</span>
972 </div>
973 </div>
974
975 <div>
976 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
977 Action Scheduler Concurrent Batches:
978 </label>
979 <div style="display: flex; align-items: center; gap: 10px;">
980 <input type="number"
981 id="action_scheduler_batches"
982 name="action_scheduler_batches"
983 value="<?php echo esc_attr($settings['action_scheduler_batches']); ?>"
984 min="1"
985 max="10"
986 required
987 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
988 </div>
989 <p style="color: #f59e0b; font-size: 12px; margin: 4px 0 0 0; display: flex; align-items: center; gap: 4px;">
990 <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;color:#f59e0b;"></span>
991 <span>Higher values increase server load</span>
992 </p>
993 </div>
994 </div>
995 </div>
996
997 <!-- API Rate Limiting Section -->
998 <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;">
999 <h3 style="color: var(--dashboard-text-primary); margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">API Rate Limiting</h3>
1000
1001 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;">
1002 <div>
1003 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1004 <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> API Calls Per Minute:
1005 </label>
1006 <div style="display: flex; align-items: center; gap: 10px;">
1007 <input type="number"
1008 id="otto_rate_limit"
1009 name="otto_rate_limit"
1010 value="<?php echo esc_attr($settings['otto_rate_limit']); ?>"
1011 min="1"
1012 max="60"
1013 required
1014 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1015 </div>
1016 </div>
1017
1018 <div>
1019 <label style="display: block; color: var(--dashboard-text-primary); margin-bottom: 8px; font-weight: 500;">
1020 Queue Auto-Cleanup:
1021 </label>
1022 <div style="display: flex; align-items: center; gap: 10px;">
1023 <input type="number"
1024 id="queue_cleanup_days"
1025 name="queue_cleanup_days"
1026 value="<?php echo esc_attr($settings['queue_cleanup_days']); ?>"
1027 min="7"
1028 max="90"
1029 required
1030 style="width: 100px; padding: 8px; border: 1px solid var(--dashboard-border); border-radius: 6px; background: var(--dashboard-card-bg); color: var(--dashboard-text-primary);" />
1031 <span style="color: var(--dashboard-text-secondary);">days</span>
1032 </div>
1033 </div>
1034 </div>
1035 </div>
1036
1037 <!-- Save Button -->
1038 <div style="margin-top: 20px;">
1039 <button type="submit"
1040 id="metasync-execution-settings-save-btn"
1041 class="button button-primary"
1042 style="padding: 10px 20px; font-size: 14px; font-weight: 500;">
1043 <span class="save-text">Save Settings</span>
1044 <span class="save-spinner" style="display: none; margin-left: 8px;"></span>
1045 </button>
1046 </div>
1047
1048 <!-- Success/Error Messages -->
1049 <div id="metasync-execution-settings-message" style="display: none; margin-top: 16px; padding: 12px; border-radius: 6px;"></div>
1050 </form>
1051 </div>
1052
1053 <?php
1054 // Real-time validation and AJAX save for this form live in
1055 // admin/js/metasync-execution-settings.js (enqueued on this page).
1056 // The previous inline duplicate was removed to avoid a double-bound
1057 // submit handler that fired the save request twice.
1058 ?>
1059 <?php
1060 }
1061
1062 public function get_field_tooltips() {
1063 $plugin_name = Metasync::get_effective_plugin_name();
1064 $otto_name = Metasync::get_whitelabel_otto_name();
1065
1066 return array(
1067 '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),
1068 '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),
1069 '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),
1070 '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),
1071 '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),
1072 '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),
1073 '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),
1074 '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),
1075 '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),
1076 '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.',
1077 '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.',
1078 '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.',
1079 '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.',
1080 '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.',
1081 '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.',
1082 '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.',
1083 '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).',
1084 '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),
1085 '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.',
1086 '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),
1087 '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),
1088 '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),
1089 '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),
1090 '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),
1091 '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),
1092 '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.'
1093 );
1094 }
1095
1096 public function get_field_section($field_id) {
1097 $sections = $this->get_accordion_sections_config();
1098
1099 foreach ($sections as $section_key => $section_data) {
1100 if (in_array($field_id, $section_data['fields'])) {
1101 return $section_key;
1102 }
1103 }
1104
1105 return null;
1106 }
1107
1108 public function render_accordion_sections($page) {
1109 global $wp_settings_sections, $wp_settings_fields;
1110
1111 if (!isset($wp_settings_fields[$page])) {
1112 return;
1113 }
1114
1115 $sections_config = $this->get_accordion_sections_config();
1116
1117 echo '<div class="metasync-settings-accordion">';
1118
1119 uasort($sections_config, function($a, $b) {
1120 return $a['priority'] - $b['priority'];
1121 });
1122
1123 foreach ($sections_config as $section_key => $section_data) {
1124 $section_id = 'metasync-section-' . $section_key;
1125 $is_open = $section_data['default_open'];
1126 $aria_expanded = $is_open ? 'true' : 'false';
1127 $content_state = $is_open ? 'open' : 'closed';
1128
1129 echo '<div class="metasync-accordion-section" data-section="' . esc_attr($section_key) . '">';
1130
1131 echo '<div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="' . $aria_expanded . '" aria-controls="' . $section_id . '">';
1132 echo '<div class="metasync-accordion-title">';
1133 echo '<span class="metasync-accordion-icon"><span class="dashicons dashicons-' . esc_attr($section_data['icon']) . '"></span></span>';
1134 echo '<div class="metasync-accordion-text">';
1135 echo '<h3>' . esc_html($section_data['title']) . '</h3>';
1136 echo '<p class="metasync-accordion-description">' . esc_html($section_data['description']) . '</p>';
1137 echo '</div>';
1138 echo '</div>';
1139 echo '<button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">';
1140 echo '<span class="toggle-icon">▼</span>';
1141 echo '</button>';
1142 echo '</div>';
1143
1144 echo '<div class="metasync-accordion-content" id="' . $section_id . '" data-state="' . $content_state . '">';
1145
1146 if (isset($section_data['render_callback']) && is_callable($section_data['render_callback'])) {
1147 call_user_func($section_data['render_callback']);
1148 } elseif (isset($section_data['fields']) && is_array($section_data['fields'])) {
1149 echo '<table class="form-table" role="presentation">';
1150
1151 $tooltips = $this->get_field_tooltips();
1152
1153 foreach ($section_data['fields'] as $field_id) {
1154 if (isset($wp_settings_fields[$page]['metasync_settings'][$field_id])) {
1155 $field = $wp_settings_fields[$page]['metasync_settings'][$field_id];
1156
1157 echo '<tr>';
1158 echo '<th scope="row">';
1159 if (!empty($field['title'])) {
1160 echo '<div class="metasync-field-label-wrapper">';
1161 echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1162
1163 if (isset($tooltips[$field_id])) {
1164 echo '<button type="button" class="metasync-tooltip-trigger" data-tooltip-id="' . esc_attr($field_id) . '" aria-label="More information">';
1165 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">';
1166 echo '<circle cx="12" cy="12" r="10"></circle>';
1167 echo '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>';
1168 echo '<line x1="12" y1="17" x2="12.01" y2="17"></line>';
1169 echo '</svg>';
1170 echo '</button>';
1171
1172 echo '<div class="metasync-tooltip" id="tooltip-' . esc_attr($field_id) . '" role="tooltip">';
1173 echo '<div class="metasync-tooltip-arrow"></div>';
1174 echo '<div class="metasync-tooltip-content">' . esc_html($tooltips[$field_id]) . '</div>';
1175 echo '</div>';
1176 }
1177
1178 echo '</div>';
1179 }
1180 echo '</th>';
1181 echo '<td>';
1182 call_user_func($field['callback'], $field['args']);
1183 echo '</td>';
1184 echo '</tr>';
1185 }
1186 }
1187
1188 echo '</table>';
1189 }
1190
1191 echo '</div>';
1192 echo '</div>';
1193 }
1194
1195 echo '</div>';
1196 }
1197
1198 public function render_llms_txt_section() {
1199 global $wp_settings_fields;
1200
1201 $page = Metasync_Admin::$page_slug . '_general';
1202 $section = Metasync_Admin::SECTION_LLMS_TXT;
1203
1204 if (empty($wp_settings_fields[$page][$section])) {
1205 return;
1206 }
1207
1208 echo '<table class="form-table" role="presentation">';
1209 foreach ($wp_settings_fields[$page][$section] as $field) {
1210 echo '<tr>';
1211 echo '<th scope="row">';
1212 if (!empty($field['title'])) {
1213 echo '<label for="' . esc_attr($field['id']) . '">' . $field['title'] . '</label>';
1214 }
1215 echo '</th>';
1216 echo '<td>';
1217 call_user_func($field['callback'], $field['args']);
1218 echo '</td>';
1219 echo '</tr>';
1220 }
1221 echo '</table>';
1222 ?>
1223 <script>
1224 (function(){
1225 function toggleLlms(){
1226 var enabled = document.getElementById('llms_txt_enabled');
1227 var fullEnabled = document.getElementById('llms_full_enabled');
1228 var deps = document.querySelectorAll('.llms-txt-dependent');
1229 var fullDeps = document.querySelectorAll('.llms-full-dependent');
1230 var i;
1231 for(i = 0; i < deps.length; i++){
1232 var row = deps[i].closest('tr');
1233 if(row) row.style.display = enabled && enabled.checked ? '' : 'none';
1234 }
1235 for(i = 0; i < fullDeps.length; i++){
1236 var row = fullDeps[i].closest('tr');
1237 if(row) row.style.display = (enabled && enabled.checked && fullEnabled && fullEnabled.checked) ? '' : 'none';
1238 }
1239 }
1240 document.addEventListener('DOMContentLoaded', function(){
1241 var enabled = document.getElementById('llms_txt_enabled');
1242 var fullEnabled = document.getElementById('llms_full_enabled');
1243 if(enabled) enabled.addEventListener('change', toggleLlms);
1244 if(fullEnabled) fullEnabled.addEventListener('change', toggleLlms);
1245 toggleLlms();
1246 });
1247 })();
1248 </script>
1249 <?php
1250 }
1251
1252 // ────────────────────────────────────────────────────────────────
1253 // Settings field callbacks
1254 // ────────────────────────────────────────────────────────────────
1255
1256 public function metasync_settings_genkey_callback()
1257 {
1258 $current_token = Metasync::get_option('general')['apikey'] ?? '';
1259
1260 if (!empty($current_token)) {
1261 $display_value = $current_token;
1262 $status_message = 'Plugin Auth Token is active and ready for authentication.';
1263 $refresh_help = 'Click refresh to generate a new token and update the heartbeat API.';
1264 } else {
1265 $display_value = 'Auto-generated when connecting to ' . esc_html(Metasync::get_effective_plugin_name());
1266 $status_message = 'Plugin Auth Token will be automatically generated when you click "Connect to ' . esc_html(Metasync::get_effective_plugin_name()) . '".';
1267 $refresh_help = 'You can also manually generate a token by clicking refresh.';
1268 }
1269
1270 printf('<div class="metasync-auth-token-row">');
1271 printf(
1272 '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" class="regular-text" readonly="readonly" />',
1273 esc_attr($display_value)
1274 );
1275 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>');
1276 printf('</div>');
1277 printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1278 }
1279
1280 public function linkgraph_token_callback()
1281 {
1282 printf(
1283 '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1284 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1285 );
1286
1287 printf(
1288 '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1289 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1290 );
1291
1292 ?>
1293 <button type="button" class="button button-primary" id="lgloginbtn">Fetch Token</button>
1294 <input type="text" id="lgusername" class="input lguser hidden" placeholder="username" />
1295 <input type="text" id="lgpassword" class="input lguser hidden" placeholder="password" />
1296 <p id="lgerror" class="notice notice-error hidden" style="display: none;"></p>
1297 <?php
1298 }
1299
1300
1301 public function time_elapsed_string($datetime, $full = false)
1302 {
1303 if(empty($datetime)){
1304 return "";
1305 }
1306 # Stored timestamps use current_time('mysql') which is the WP site's
1307 # local timezone (no offset marker on the string). Constructing
1308 # DateTime without a timezone would default to UTC and produce a
1309 # bogus diff equal to the WP timezone offset (QA: "4 hours ago"
1310 # after a 4-minute sync on non-UTC sites).
1311 $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
1312 $now = new DateTime('now', $wp_tz);
1313 $ago = new DateTime($datetime, $wp_tz);
1314
1315 $diff = $now->diff($ago);
1316
1317 $string = [
1318 'y' => 'year',
1319 'm' => 'month',
1320 'w' => 'week',
1321 'd' => 'day',
1322 'h' => 'hour',
1323 'i' => 'minute',
1324 's' => 'second',
1325 ];
1326
1327 foreach ($string as $k => &$v) {
1328 if (isset($diff->$k) && $diff->$k) {
1329 $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
1330 } else {
1331 unset($string[$k]);
1332 }
1333 }
1334 if (!$full) $string = array_slice($string, 0, 1);
1335 return $string ? implode(', ', $string) . ' ago' : 'just now';
1336 }
1337
1338 public function searchatlas_api_key_callback()
1339 {
1340 $current_api_key = isset(Metasync::get_option('general')['searchatlas_api_key']) ? esc_attr(Metasync::get_option('general')['searchatlas_api_key']) : '';
1341 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1342
1343 $has_api_key = !empty($current_api_key);
1344 $has_otto_uuid = !empty($otto_uuid);
1345
1346 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1347
1348 printf('<div class="metasync-sa-connect-container">');
1349
1350 printf('<div class="metasync-sa-connect-title">');
1351 printf('One-Click Authentication');
1352 printf('</div>');
1353
1354 printf('<div class="metasync-sa-connect-description">');
1355 if ($is_fully_connected) {
1356 printf('Your %s account is fully synced with active heartbeat API. You can re-authenticate to refresh your connection or connect a different account.', esc_html(Metasync::get_effective_plugin_name()));
1357 } elseif ($has_api_key && !$has_otto_uuid) {
1358 printf('Your %s API key is configured, but %s UUID is missing. Please re-authenticate to complete the setup.', esc_html(Metasync::get_effective_plugin_name()), esc_html(Metasync::get_whitelabel_otto_name()));
1359 } else {
1360 printf('Connect your %s account with one click. This will automatically configure your API key and %s UUID below, enabling all plugin features.', esc_html(Metasync::get_effective_plugin_name()), esc_html(Metasync::get_whitelabel_otto_name()));
1361 }
1362 printf('</div>');
1363
1364 ?>
1365 <div class="metasync-mcp-consent" style="margin-top: 15px; padding: 12px 15px; background: #f0f9ff; border-left: 3px solid #0ea5e9; border-radius: 4px;">
1366 <p style="margin: 0; color: #334155; font-size: 13px; line-height: 1.6;">
1367 <strong>AI-Powered SEO Automation:</strong> By authenticating, you authorize <strong><?php echo esc_html(Metasync::get_effective_plugin_name()); ?> Brain</strong> (our AI assistant) to access your WordPress admin capabilities through the Model Context Protocol (MCP). This enables intelligent automation for SEO optimizations, content enhancements, and performance improvements.
1368 </p>
1369 </div>
1370 <?php
1371
1372 printf('<div class="metasync-sa-connect-buttons">');
1373
1374 printf('<button type="button" id="connect-searchatlas-btn" class="metasync-sa-connect-btn">');
1375 if ($is_fully_connected) {
1376 printf('Re-authenticate with %s', esc_html(Metasync::get_effective_plugin_name()));
1377 } elseif ($has_api_key && !$has_otto_uuid) {
1378 printf('Complete Authentication Setup');
1379 } else {
1380 printf('Connect to %s', esc_html(Metasync::get_effective_plugin_name()));
1381 }
1382 printf('</button>');
1383
1384 if ($has_api_key) {
1385 printf('<button type="button" id="reset-searchatlas-auth" class="metasync-sa-reset-btn" style="margin-left: 10px;">');
1386 printf('Disconnect Account');
1387 printf('</button>');
1388 }
1389
1390 printf('</div>');
1391
1392 printf('<div style="margin-top: 15px;">');
1393 printf('<details style="margin-top: 10px;">');
1394 printf('<summary style="cursor: pointer; color: #666; font-size: 13px;">Authentication Tips</summary>');
1395 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1396 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1397 printf('• The authentication window will open in a popup - please allow popups<br/>');
1398 printf('• The process typically takes 15-30 seconds to complete<br/>');
1399 printf('• Your API key will be automatically filled in the field below<br/>');
1400 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1401 printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL);
1402 printf('</div>');
1403 printf('</details>');
1404 printf('</div>');
1405
1406 printf('</div>');
1407
1408 printf('<div style="margin-top: 20px;">');
1409 printf('<label for="searchatlas-api-key" style="font-weight: 600; display: block; margin-bottom: 8px;">');
1410 printf('%s API Key', esc_html(Metasync::get_effective_plugin_name()));
1411 if ($is_fully_connected) {
1412 printf('<span style="color: #46b450; margin-left: 10px; font-weight: normal;">✓ Synced</span>');
1413 } elseif ($has_api_key && !$has_otto_uuid) {
1414 printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name()));
1415 }
1416 printf('</label>');
1417
1418 printf(
1419 '<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" />',
1420 $current_api_key
1421 );
1422
1423 printf('<p class="description" style="margin-top: 8px;">');
1424 if ($is_fully_connected) {
1425 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()));
1426 } elseif ($has_api_key && !$has_otto_uuid) {
1427 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()));
1428 } else {
1429 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.');
1430 }
1431 printf('</p>');
1432
1433 ?>
1434 <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;">
1435 <strong>Manual Authentication:</strong> If you manually enter your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> API Key and <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> UUID, you also consent to the same AI-powered automation permissions described above.
1436 </p>
1437 <?php
1438
1439 printf('</div>');
1440
1441 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1442 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1443 printf(
1444 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1445 esc_attr($timestamp),
1446 $this->time_elapsed_string($timestamp)
1447 );
1448
1449
1450 }
1451 }
1452
1453 public function bing_site_verification_callback()
1454 {
1455 printf(
1456 '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1457 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1458 );
1459
1460 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
1461 printf(' <a href="https://www.bing.com/webmasters/about" target="_blank">Get from here</a> <br> ');
1462 }
1463
1464 public function yandex_site_verification_callback()
1465 {
1466 printf(
1467 '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1468 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1469 );
1470
1471 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
1472 printf(' <a href="https://passport.yandex.com/auth" target="_blank">Get from here</a> <br> ');
1473 }
1474
1475 public function google_site_verification_callback()
1476 {
1477 printf(
1478 '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />',
1479 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1480 );
1481
1482 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
1483 printf(' <a href="https://www.google.com/webmasters/verification" target="_blank">Get from here</a> <br> ');
1484 }
1485
1486 public function pinterest_site_verification_callback()
1487 {
1488 printf(
1489 '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1490 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1491 );
1492
1493 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
1494 printf(' <a href="https://in.pinterest.com/" target="_blank">Get from here</a> <br> ');
1495 }
1496
1497 public function local_seo_person_organization_callback()
1498 {
1499 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1500 ?>
1501 <div class="metasync-type-select-wrap">
1502 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1503 <?php
1504 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1505 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1506 ?>
1507 </select>
1508 </div>
1509 <?php
1510 printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>');
1511 }
1512
1513 public function local_seo_name_callback()
1514 {
1515 printf(
1516 '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1517 isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo()
1518 );
1519
1520 printf(' <br> <span class="description"> Your name or company name </span>');
1521 }
1522
1523 public function local_seo_logo_callback()
1524 {
1525 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1526
1527 printf(
1528 '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1529 isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : ''
1530 );
1531
1532 printf(' <br> <input class="button-secondary" type="button" id="logo_upload_button" value="Add or Upload File">');
1533
1534 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> ');
1535
1536 printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? '');
1537
1538 $button_type = 'hidden';
1539 if ($local_seo_logo) {
1540 $button_type = 'button';
1541 }
1542 printf('<input type="%s" class="button-secondary" id="local_seo_logo_close_btn" value="X">', $button_type);
1543 }
1544
1545 public function local_seo_url_callback()
1546 {
1547 printf(
1548 '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1549 isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url()
1550 );
1551
1552 printf(' <br> <span class="description"> URL of the item. </span>');
1553 }
1554
1555 public function local_seo_email_callback()
1556 {
1557 printf(
1558 '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />',
1559 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1560 );
1561
1562 printf(' <br> <span class="description"> Search engines display your email address. </span>');
1563 }
1564
1565 public function local_seo_phone_callback()
1566 {
1567 printf(
1568 '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />',
1569 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1570 );
1571
1572 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1573 }
1574
1575 public function local_seo_address_callback()
1576 {
1577 printf(
1578 '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1579 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1580 );
1581
1582 printf(
1583 '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1584 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1585 );
1586
1587 printf(
1588 '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1589 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1590 );
1591
1592 printf(
1593 '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1594 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1595 );
1596
1597 printf(
1598 '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1599 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1600 );
1601 }
1602
1603 public function local_seo_business_type_callback()
1604 {
1605 $types = self::get_business_types();
1606 sort($types);
1607
1608 $business_type = Metasync::get_option('localseo')['local_seo_business_type'] ?? '';
1609
1610 ?>
1611 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1612 <option value='0'>Select Business Type</option>
1613 <?php
1614 foreach ($types as $type) {
1615 printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type);
1616 }
1617 ?>
1618 </select>
1619 <?php
1620 }
1621
1622 public function local_seo_opening_hours_callback()
1623 {
1624 $days_name = ['Monday', 'Tuseday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
1625 $days = isset(Metasync::get_option('localseo')['days']) ? Metasync::get_option('localseo')['days'] : '';
1626 $times = isset(Metasync::get_option('localseo')['times']) ? Metasync::get_option('localseo')['times'] : '';
1627
1628 ?>
1629 <ul id="daysTime">
1630 <?php
1631 $opening_days = [];
1632 if ($days && $times) {
1633 $opening_days = array_combine($days, $times);
1634 }
1635 foreach ($opening_days as $day_name => $day_time) {
1636 ?>
1637 <li>
1638 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1639 <?php
1640 foreach ($days_name as $name) {
1641 printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name));
1642 }
1643 ?>
1644 </select>
1645 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
1646 <button id="timeDelete">Delete</button>
1647 </li>
1648 <?php } ?>
1649 <?php if (empty($opening_days)) { ?>
1650 <li>
1651 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1652 <?php
1653 foreach ($days_name as $name) {
1654 printf('<option value="%s" >%s</option>', esc_attr($name), esc_attr($name));
1655 }
1656 ?>
1657 </select>
1658 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="">
1659 <button id="timeDelete">Delete</button>
1660 </li>
1661 <?php } ?>
1662 </ul>
1663 <?php
1664
1665 printf(' <input type="hidden" id="days_time_count" value="%s"/>', count($opening_days));
1666 printf(' <input class="button-secondary" type="button" id="addNewTime" value="Add Time">');
1667 printf(' <br> <span class="description"> Select opening hours. You can add multiple sets if you have different opening or closing hours on some days or if you have a mid-day break. Times are specified using 24:00 time. </span>');
1668 }
1669
1670 public function local_seo_phone_numbers_callback()
1671 {
1672 $number_types = ['Customer Service', 'Technical Support', 'Billing Support', 'Bill Payment', 'Sales', 'Reservations', 'Credit Card Support', 'Emergency', 'Baggage Tracking', 'Roadside Assistance', 'Package Tracking'];
1673 $types = isset(Metasync::get_option('localseo')['phonetype']) ? Metasync::get_option('localseo')['phonetype'] : '';
1674 $numbers = isset(Metasync::get_option('localseo')['phonenumber']) ? Metasync::get_option('localseo')['phonenumber'] : '';
1675
1676 ?>
1677
1678 <ul id="phone-numbers">
1679 <?php
1680 $phone_numbers = [];
1681 if ($types && $numbers) {
1682 $phone_numbers = array_combine($types, $numbers);
1683 }
1684 foreach ($phone_numbers as $phone_type => $phone_number) {
1685 ?>
1686 <li>
1687 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1688 <?php
1689 foreach ($number_types as $type) {
1690 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($phone_type)), esc_attr($type));
1691 }
1692 ?>
1693 </select>
1694 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="<?php echo esc_attr($phone_number) ?>">
1695 <button id="number-delete">Delete</button>
1696 </li>
1697 <?php } ?>
1698 <?php if (empty($phone_numbers)) { ?>
1699 <li>
1700 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1701 <?php
1702 foreach ($number_types as $type) {
1703 printf('<option value="%s" >%s</option>', esc_attr($type), esc_attr($type));
1704 }
1705 ?>
1706 </select>
1707 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="">
1708 <button id="number-delete">Delete</button>
1709 </li>
1710 <?php } ?>
1711 </ul>
1712 <?php
1713
1714 printf(' <input type="hidden" id="phone_number_count" value="%s"/>', count($phone_numbers));
1715 printf(' <input class="button-secondary" type="button" id="addNewNumber" value="Add Number">');
1716 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1717 }
1718
1719 public function local_seo_price_range_callback()
1720 {
1721 printf(
1722 '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1723 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1724 );
1725 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1726 }
1727
1728 public function local_seo_about_page_callback()
1729 {
1730 ?>
1731 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_about_page]') ?>">
1732 <option value='0'>Select About Page</option>
1733 <?php
1734 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1735 $pages = get_pages();
1736 foreach ($pages as $page) {
1737 printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title);
1738 }
1739 ?>
1740 </select>
1741 <?php
1742 printf(' <br> <span class="description"> Search engines tag your about us page. </span>');
1743 }
1744
1745 public function local_seo_contact_page_callback()
1746 {
1747 ?>
1748 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_contact_page]') ?>">
1749 <option value='0'>Select Contact Page</option>
1750 <?php
1751 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1752 $pages = get_pages();
1753 foreach ($pages as $page) {
1754 printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title);
1755 }
1756 ?>
1757 </select>
1758 <?php
1759 printf(' <br> <span class="description"> Search engines tag your contact page. </span>');
1760 }
1761
1762 public function local_seo_map_key_callback()
1763 {
1764 printf(
1765 '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1766 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1767 );
1768
1769 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
1770 }
1771
1772 public function local_seo_geo_coordinates_callback()
1773 {
1774 printf(
1775 '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1776 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1777 );
1778
1779 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
1780 }
1781
1782 public function header_snippets_callback()
1783 {
1784 printf(
1785 '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>',
1786 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1787 );
1788 }
1789
1790 public function footer_snippets_callback()
1791 {
1792 printf(
1793 '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>',
1794 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1795 );
1796 }
1797
1798 public function no_index_posts_callback()
1799 {
1800 printf(
1801 '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />',
1802 isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : ''
1803 );
1804
1805 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>');
1806 }
1807
1808 public function no_follow_links_callback()
1809 {
1810 printf(
1811 '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />',
1812 isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : ''
1813 );
1814
1815 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>');
1816 }
1817
1818 public function open_external_links_callback()
1819 {
1820 printf(
1821 '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />',
1822 isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : ''
1823 );
1824
1825 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>');
1826 }
1827
1828 public function add_alt_image_tags_callback()
1829 {
1830 printf(
1831 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />',
1832 isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : ''
1833 );
1834
1835 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>');
1836 }
1837
1838 public function add_title_image_tags_callback()
1839 {
1840 printf(
1841 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />',
1842 isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : ''
1843 );
1844
1845 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>');
1846 }
1847
1848 public function site_type_callback()
1849 {
1850
1851 $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? '';
1852
1853 $types = [
1854 ['name' => 'Personal Blog', 'value' => 'blog'],
1855 ['name' => 'Community Blog/News Site', 'value' => 'news'],
1856 ['name' => 'Personal Portfolio', 'value' => 'portfolio'],
1857 ['name' => 'Small Business Site', 'value' => 'business'],
1858 ['name' => 'Webshop', 'value' => 'webshop'],
1859 ['name' => 'Other Personal Website', 'value' => 'otherpersonal'],
1860 ['name' => 'Other Business Website', 'value' => 'otherbusiness'],
1861 ];
1862
1863 ?>
1864 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type">
1865 <?php
1866 foreach ($types as $type) {
1867 printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name']));
1868 }
1869 ?>
1870 </select>
1871 <?php
1872
1873 }
1874
1875 public function site_business_type_callback()
1876 {
1877
1878 $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? '';
1879
1880 $types = self::get_business_types();
1881 sort($types);
1882
1883 ?>
1884 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>">
1885 <option value='0'>Select Business Type</option>
1886 <?php
1887 foreach ($types as $type) {
1888 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type));
1889 }
1890 ?>
1891 </select>
1892 <?php
1893 }
1894
1895 public function site_company_name_callback()
1896 {
1897
1898 $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name');
1899
1900 printf(
1901 '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />',
1902 $company_name ? $company_name : get_bloginfo('name')
1903 );
1904 }
1905
1906 public function site_google_logo_callback()
1907 {
1908
1909 $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? '';
1910
1911 printf(
1912 '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />',
1913 $google_logo
1914 );
1915
1916 printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">');
1917 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> ');
1918 printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? '');
1919
1920 $button_type = 'hidden';
1921 if ($google_logo) {
1922 $button_type = 'button';
1923 }
1924 printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type);
1925 }
1926
1927 public function site_social_share_image_callback()
1928 {
1929
1930 $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? '';
1931
1932 printf(
1933 '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />',
1934 $social_share_image
1935 );
1936
1937 printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">');
1938 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> ');
1939 printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? '');
1940
1941 $button_type = 'hidden';
1942 if ($social_share_image) {
1943 $button_type = 'button';
1944 }
1945 printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type);
1946 }
1947
1948 public function common_robot_meta_tags_callback()
1949 {
1950 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
1951
1952 ?>
1953 <ul class="checkbox-list">
1954 <li>
1955 <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']) : '' ?>>
1956 <label for="robots_common1">Index </br>
1957 <span class="description">
1958 <span>Search engines to index and show these pages in the search results.</span>
1959 </span>
1960 </label>
1961 </li>
1962 <li>
1963 <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']) : '' ?>>
1964 <label for="robots_common2">No Index </br>
1965 <span class="description">
1966 <span>Search engines not indexed and displayed this pages in search engine results</span>
1967 </span>
1968 </label>
1969 </li>
1970 <li>
1971 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][nofollow]') ?>" id="robots_common3" value="nofollow" <?php isset($common_robots_meta['nofollow']) ? checked('nofollow', $common_robots_meta['nofollow']) : '' ?>>
1972 <label for="robots_common3">No Follow </br>
1973 <span class="description">
1974 <span>Search engines not follow the links on the pages</span>
1975 </span>
1976 </label>
1977 </li>
1978 <li>
1979 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noarchive]') ?>" id="robots_common4" value="noarchive" <?php isset($common_robots_meta['noarchive']) ? checked('noarchive', $common_robots_meta['noarchive']) : '' ?>>
1980 <label for="robots_common4">No Archive </br>
1981 <span class="description">
1982 <span>Search engines not showing Cached links for pages</span>
1983 </span>
1984 </label>
1985 </li>
1986 <li>
1987 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][noimageindex]') ?>" id="robots_common5" value="noimageindex" <?php isset($common_robots_meta['noimageindex']) ? checked('noimageindex', $common_robots_meta['noimageindex']) : '' ?>>
1988 <label for="robots_common5">No Image Index </br>
1989 <span class="description">
1990 <span>If you do not want to apear your pages as the referring page for images that appear in image search results</span>
1991 </span>
1992 </label>
1993 </li>
1994 <li>
1995 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[common_robots_meta][nosnippet]') ?>" id="robots_common6" value="nosnippet" <?php isset($common_robots_meta['nosnippet']) ? checked('nosnippet', $common_robots_meta['nosnippet']) : '' ?>>
1996 <label for="robots_common6">No Snippet </br>
1997 <span class="description">
1998 <span>Search engines not snippet to show in the search results</span>
1999 </span>
2000 </label>
2001 </li>
2002 </ul>
2003 <?php
2004 }
2005
2006 /**
2007 * @deprecated Use common_robot_meta_tags_callback() instead
2008 */
2009 public function common_robot_mata_tags_callback()
2010 {
2011 return $this->common_robot_meta_tags_callback();
2012 }
2013
2014 public function advance_robot_meta_tags_callback()
2015 {
2016 $advance_robots_meta = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
2017
2018 $snippet_advance_robots_enable = $advance_robots_meta['max-snippet']['enable'] ?? '';
2019 $snippet_advance_robots_length = $advance_robots_meta['max-snippet']['length'] ?? '-1';
2020 $video_advance_robots_enable = $advance_robots_meta['max-video-preview']['enable'] ?? '';
2021 $video_advance_robots_length = $advance_robots_meta['max-video-preview']['length'] ?? '-1';
2022 $image_advance_robots_enable = $advance_robots_meta['max-image-preview']['enable'] ?? '';
2023 $image_advance_robots_length = $advance_robots_meta['max-image-preview']['length'] ?? '';
2024
2025 ?>
2026 <ul class="checkbox-list">
2027 <li>
2028 <label for="advanced_robots_snippet">
2029 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-snippet][enable]') ?>" id="advanced_robots_snippet" value="1" <?php checked('1', esc_attr($snippet_advance_robots_enable)) ?>>
2030 Snippet </br>
2031 <input type="number" class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-snippet][length]') ?>" id="advanced_robots_snippet_value" value="<?php echo esc_attr($snippet_advance_robots_length); ?>" min="-1"> </br>
2032 <span class="description">
2033 <span>Add maximum text-length, in characters, of a snippet for your page.</span>
2034 </span>
2035 </label>
2036 </li>
2037 <li>
2038 <label for="advanced_robots_video">
2039 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-video-preview][enable]') ?>" id="advanced_robots_video" value="1" <?php checked('1', esc_attr($video_advance_robots_enable)) ?>>
2040 Video Preview </br>
2041 <input type="number" class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-video-preview][length]') ?>" id="advanced_robots_video_value" value="<?php echo esc_attr($video_advance_robots_length); ?>" min="-1"> </br>
2042 <span class="description">
2043 <span>Add maximum duration in seconds of an animated video preview.</span>
2044 </span>
2045 </label>
2046 </li>
2047 <li>
2048 <label for="advanced_robots_image">
2049 <input type="checkbox" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-image-preview][enable]') ?>" id="advanced_robots_image" value="1" <?php checked('1', esc_attr($image_advance_robots_enable)); ?>>
2050 Image Preview </br>
2051 <select class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[advance_robots_meta][max-image-preview][length]') ?>" id="advanced_robots_image_value">
2052 <option value="large" <?php selected('large', esc_attr($image_advance_robots_length)) ?>>Large</option>
2053 <option value="standard" <?php selected('standard', esc_attr($image_advance_robots_length)) ?>>Standard</option>
2054 <option value="none" <?php selected('none', esc_attr($image_advance_robots_length)) ?>>None</option>
2055 </select>
2056 </br>
2057 <span class="description">
2058 <span>Add maximum size of image preview to show the images on this page.</span>
2059 </span>
2060 </label>
2061 </li>
2062 </ul>
2063 <?php
2064 }
2065
2066 /**
2067 * @deprecated Use advance_robot_meta_tags_callback() instead
2068 */
2069 public function advance_robot_mata_tags_callback()
2070 {
2071 return $this->advance_robot_meta_tags_callback();
2072 }
2073
2074 public function global_twitter_card_type_callback()
2075 {
2076 $twitter_card_type = Metasync::get_option('twitter_card_type') ?? '';
2077 ?>
2078
2079 <select class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[twitter_card_type]') ?>" id="twitter_card_type">
2080 <option value="summary_large_image" <?php selected('summary_large_image', esc_attr($twitter_card_type)) ?>>Summary Large Image</option>
2081 <option value="summary_card" <?php selected('summary_card', esc_attr($twitter_card_type)) ?>>Summary Card</option>
2082 </select>
2083
2084 <?php
2085 }
2086
2087 public function global_open_graph_meta_callback()
2088 {
2089 printf(
2090 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2091 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2092 );
2093 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2094 }
2095
2096 public function global_facebook_meta_callback()
2097 {
2098 printf(
2099 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2100 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2101 );
2102 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2103 }
2104
2105 public function global_twitter_meta_callback()
2106 {
2107 printf(
2108 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2109 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2110 );
2111 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2112 }
2113
2114 public function og_image_dimensions_callback()
2115 {
2116 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2117 printf(
2118 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2119 $val === 'true' ? 'checked' : ''
2120 );
2121 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2122 }
2123
2124 public function article_timestamps_callback()
2125 {
2126 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2127 printf(
2128 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />',
2129 $val === 'true' ? 'checked' : ''
2130 );
2131 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2132 }
2133
2134 public function article_author_callback()
2135 {
2136 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2137 printf(
2138 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />',
2139 $val === 'true' ? 'checked' : ''
2140 );
2141 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>');
2142 }
2143
2144 public function article_section_callback()
2145 {
2146 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2147 printf(
2148 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />',
2149 $val === 'true' ? 'checked' : ''
2150 );
2151 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2152 }
2153
2154 public function article_tags_callback()
2155 {
2156 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2157 printf(
2158 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />',
2159 $val === 'true' ? 'checked' : ''
2160 );
2161 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2162 }
2163
2164 public function twitter_image_alt_callback()
2165 {
2166 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2167 printf(
2168 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2169 $val === 'true' ? 'checked' : ''
2170 );
2171 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2172 }
2173
2174 public function facebook_page_url_callback()
2175 {
2176 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2177 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2178 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2179 }
2180
2181 public function facebook_authorship_callback()
2182 {
2183 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2184 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2185 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>');
2186 }
2187
2188 public function facebook_admin_callback()
2189 {
2190 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2191 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2192 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2193 }
2194
2195 public function facebook_app_callback()
2196 {
2197 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2198 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2199 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2200 }
2201
2202 public function facebook_secret_callback()
2203 {
2204 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2205 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2206 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2207 }
2208
2209 public function twitter_username_callback()
2210 {
2211 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2212 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2213 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2214 }
2215
2216 public static function get_business_types()
2217 {
2218 $business_type = [
2219 'Airline',
2220 'Consortium',
2221 'Corporation',
2222 'Educational Organization',
2223 'College Or University',
2224 'Elementary School',
2225 'High School',
2226 'Middle School',
2227 'Preschool',
2228 'School',
2229 'Funding Scheme',
2230 'Government Organization',
2231 'Library System',
2232 'Local Business',
2233 'Animal Shelter',
2234 'Archive Organization',
2235 'Automotive Business',
2236 'Auto Body Shop',
2237 'Auto Dealer',
2238 'Auto Parts Store',
2239 'Auto Rental',
2240 'Auto Repair',
2241 'Auto Wash',
2242 'Gas Station',
2243 'Motorcycle Dealer',
2244 'Motorcycle Repair',
2245 'Child Care',
2246 'Dry Cleaning Or Laundry',
2247 'Emergency Service',
2248 'Fire Station',
2249 'Hospital',
2250 'Police Station',
2251 'Employment Agency',
2252 'Entertainment Business',
2253 'Adult Entertainment',
2254 'Amusement Park',
2255 'Art Gallery',
2256 'Casino',
2257 'Comedy Club',
2258 'Movie Theater',
2259 'Night Club',
2260 'Financial Service',
2261 'Accounting Service',
2262 'Automated Teller',
2263 'Bank Or CreditUnion',
2264 'Insurance Agency',
2265 'Food Establishment',
2266 'Bakery',
2267 'Bar Or Pub',
2268 'Brewery',
2269 'Cafe Or CoffeeShop',
2270 'Distillery',
2271 'Fast Food Restaurant',
2272 'IceCream Shop',
2273 'Restaurant',
2274 'Winery',
2275 'Government Office',
2276 'Post Office',
2277 'Health And Beauty Business',
2278 'Beauty Salon',
2279 'Day Spa',
2280 'Hair Salon',
2281 'Health Club',
2282 'Nail Salon',
2283 'Tattoo Parlor',
2284 'Home And Construction Business',
2285 'Electrician',
2286 'General Contractor',
2287 'HVAC Business',
2288 'House Painter',
2289 'Locksmith',
2290 'Moving Company',
2291 'Plumber',
2292 'Roofing Contractor',
2293 'Internet Cafe',
2294 'Legal Service',
2295 'Attorney',
2296 'Notary',
2297 'Library',
2298 'Lodging Business',
2299 'Bed And Breakfast',
2300 'Campground',
2301 'Hostel',
2302 'Hotel',
2303 'Motel',
2304 'Resort',
2305 'Ski Resort',
2306 'Medical Business',
2307 'Community Health',
2308 'Dentist',
2309 'Dermatology',
2310 'Diet Nutrition',
2311 'Emergency',
2312 'Geriatric',
2313 'Gynecologic',
2314 'Medical Clinic',
2315 'Optician',
2316 'Pharmacy',
2317 'Physician',
2318 'Professional Service',
2319 'Radio Station',
2320 'Real Estate Agent',
2321 'Recycling Center',
2322 'Self Storage',
2323 'Shopping Center',
2324 'Sports Activity Location',
2325 'Bowling Alley',
2326 'Exercise Gym',
2327 'Golf Course',
2328 'Public Swimming Pool',
2329 'Ski Resort',
2330 'Sports Club',
2331 'Stadium Or Arena',
2332 'Tennis Complex',
2333 'Store',
2334 'Bike Store',
2335 'Book Store',
2336 'Clothing Store',
2337 'Computer Store',
2338 'Convenience Store',
2339 'Department Store',
2340 'Electronics Store',
2341 'Florist',
2342 'Furniture Store',
2343 'Garden Store',
2344 'Grocery Store',
2345 'Hardware Store',
2346 'Hobby Shop',
2347 'Home Goods Store',
2348 'Jewelry Store',
2349 'Liquor Store',
2350 'Mens Clothing Store',
2351 'Mobile Phone Store',
2352 'Movie Rental Store',
2353 'Music Store',
2354 'Office Equipment Store',
2355 'Outlet Store',
2356 'Pawn Shop',
2357 'Pet Store',
2358 'Shoe Store',
2359 'Sporting GoodsStore',
2360 'Tire Shop',
2361 'Toy Store',
2362 'Wholesale Store',
2363 'Television Station',
2364 'Tourist Information Center',
2365 'Travel Agency',
2366 'Tree Services',
2367 'Medical Organization',
2368 'Diagnostic Lab',
2369 'Veterinary Care',
2370 'NGO',
2371 'News Media Organization',
2372 'Performing Group',
2373 'Dance Group',
2374 'Music Group',
2375 'Theater Group',
2376 'Project',
2377 'Funding Agency',
2378 'Research Project',
2379 'Sports Organization',
2380 'Sports Team',
2381 'Workers Union',
2382 ];
2383
2384 return $business_type;
2385 }
2386
2387 /**
2388 * Render breadcrumbs settings section
2389 */
2390 public function render_breadcrumbs_section() {
2391 $options = Metasync::get_option('breadcrumbs', array());
2392
2393 $defaults = array(
2394 'enabled' => true,
2395 'separator' => '&raquo;',
2396 'home_label' => 'Home',
2397 'home_url' => '',
2398 'show_current_page' => true,
2399 'prefix_text' => '',
2400 'archive_label_format' => '{name}',
2401 'disable_schema' => false,
2402 );
2403
2404 $options = wp_parse_args($options, $defaults);
2405 ?>
2406 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2407
2408 <!-- Enable/Disable -->
2409 <div style="margin-bottom: 24px;">
2410 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2411 <input type="checkbox"
2412 name="metasync_options[breadcrumbs][enabled]"
2413 value="1"
2414 <?php checked($options['enabled'], 1); ?>
2415 style="width: 18px; height: 18px; cursor: pointer;">
2416 <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span>
2417 </label>
2418 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2419 Enable or disable breadcrumb navigation on your site
2420 </p>
2421 </div>
2422
2423 <!-- Disable Schema Markup -->
2424 <div style="margin-bottom: 24px;">
2425 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2426 <input type="checkbox"
2427 name="metasync_options[breadcrumbs][disable_schema]"
2428 value="1"
2429 <?php checked($options['disable_schema'], 1); ?>
2430 style="width: 18px; height: 18px; cursor: pointer;">
2431 <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span>
2432 </label>
2433 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2434 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;">&lt;head&gt;</code>. Use this if another SEO plugin already handles breadcrumb schema.
2435 </p>
2436 </div>
2437
2438 <!-- Separator Character -->
2439 <div style="margin-bottom: 24px;">
2440 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2441 Separator Character
2442 </label>
2443 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2444 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2445 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2446 <span style="color: var(--dashboard-text);">»</span>
2447 </label>
2448 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2449 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2450 <span style="color: var(--dashboard-text);">/</span>
2451 </label>
2452 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2453 <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2454 <span style="color: var(--dashboard-text);">></span>
2455 </label>
2456 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2457 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2458 <span style="color: var(--dashboard-text);">·</span>
2459 </label>
2460 </div>
2461 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2462 Choose how to separate breadcrumb items
2463 </p>
2464 </div>
2465
2466 <!-- Home Label -->
2467 <div style="margin-bottom: 24px;">
2468 <label for="breadcrumb_home_label" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2469 Home Label
2470 </label>
2471 <input type="text"
2472 id="breadcrumb_home_label"
2473 name="metasync_options[breadcrumbs][home_label]"
2474 value="<?php echo esc_attr($options['home_label']); ?>"
2475 placeholder="Home"
2476 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2477 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2478 Label for the home/root breadcrumb item
2479 </p>
2480 </div>
2481
2482 <!-- Home URL -->
2483 <div style="margin-bottom: 24px;">
2484 <label for="breadcrumb_home_url" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2485 Home URL
2486 </label>
2487 <input type="url"
2488 id="breadcrumb_home_url"
2489 name="metasync_options[breadcrumbs][home_url]"
2490 value="<?php echo esc_attr($options['home_url']); ?>"
2491 placeholder="<?php echo esc_attr(home_url('/')); ?>"
2492 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2493 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2494 Leave empty to use your site home URL
2495 </p>
2496 </div>
2497
2498 <!-- Show Current Page -->
2499 <div style="margin-bottom: 24px;">
2500 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2501 <input type="checkbox"
2502 name="metasync_options[breadcrumbs][show_current_page]"
2503 value="1"
2504 <?php checked($options['show_current_page'], 1); ?>
2505 style="width: 18px; height: 18px; cursor: pointer;">
2506 <span style="font-weight: 500; color: var(--dashboard-text);">Show current page</span>
2507 </label>
2508 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2509 Include the current page as the last (unlinked) breadcrumb item
2510 </p>
2511 </div>
2512
2513 <!-- Prefix Text -->
2514 <div style="margin-bottom: 24px;">
2515 <label for="breadcrumb_prefix" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2516 Prefix Text
2517 </label>
2518 <input type="text"
2519 id="breadcrumb_prefix"
2520 name="metasync_options[breadcrumbs][prefix_text]"
2521 value="<?php echo esc_attr($options['prefix_text']); ?>"
2522 placeholder='e.g., "You are here:"'
2523 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2524 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2525 Optional text to display before the breadcrumbs (leave empty for none)
2526 </p>
2527 </div>
2528
2529 <!-- Archive Label Format -->
2530 <div style="margin-bottom: 24px;">
2531 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2532 Archive Label Format
2533 </label>
2534 <input type="text"
2535 id="breadcrumb_archive_format"
2536 name="metasync_options[breadcrumbs][archive_label_format]"
2537 value="<?php echo esc_attr($options['archive_label_format']); ?>"
2538 placeholder="{name}"
2539 style="width: 100%; padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text); font-size: 14px; box-sizing: border-box;">
2540 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2541 Use <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">{name}</code> as a placeholder for the archive name. Example: <code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">Category: {name}</code>
2542 </p>
2543 </div>
2544
2545 </div>
2546 <?php
2547 }
2548 }
2549