PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.16
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.16
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 2.5.23 All 138 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.16, at includes/class-metasync-settings-fields.php

2,548 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 (WP-430).
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(
1271 '<input type="text" id="apikey" name="' . Metasync_Admin::option_key . '[general][apikey]" value="%s" size="40" readonly="readonly" /> ',
1272 esc_attr($display_value)
1273 );
1274
1275 printf('<button type="button" id="refresh-plugin-auth-token" class="button button-secondary" style="margin-left: 10px; margin-top: 8px;"><span class="dashicons dashicons-controls-repeat"></span> Refresh Token</button>');
1276 printf('<p class="description">%s %s</p>', $status_message, $refresh_help);
1277 }
1278
1279 public function linkgraph_token_callback()
1280 {
1281 printf(
1282 '<input type="text" id="linkgraph_token" name="' . Metasync_Admin::option_key . '[general][linkgraph_token]" value="%s" size="25" readonly="readonly" />',
1283 isset(Metasync::get_option('general')['linkgraph_token']) ? esc_attr(Metasync::get_option('general')['linkgraph_token']) : ''
1284 );
1285
1286 printf(
1287 '<input type="text" id="linkgraph_customer_id" name="' . Metasync_Admin::option_key . '[general][linkgraph_customer_id]" value="%s" size="25" readonly="readonly" />',
1288 isset(Metasync::get_option('general')['linkgraph_customer_id']) ? esc_attr(Metasync::get_option('general')['linkgraph_customer_id']) : ''
1289 );
1290
1291 ?>
1292 <button type="button" class="button button-primary" id="lgloginbtn">Fetch Token</button>
1293 <input type="text" id="lgusername" class="input lguser hidden" placeholder="username" />
1294 <input type="text" id="lgpassword" class="input lguser hidden" placeholder="password" />
1295 <p id="lgerror" class="notice notice-error hidden" style="display: none;"></p>
1296 <?php
1297 }
1298
1299
1300 public function time_elapsed_string($datetime, $full = false)
1301 {
1302 if(empty($datetime)){
1303 return "";
1304 }
1305 # Stored timestamps use current_time('mysql') which is the WP site's
1306 # local timezone (no offset marker on the string). Constructing
1307 # DateTime without a timezone would default to UTC and produce a
1308 # bogus diff equal to the WP timezone offset (WP-350 QA: "4 hours ago"
1309 # after a 4-minute sync on non-UTC sites).
1310 $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
1311 $now = new DateTime('now', $wp_tz);
1312 $ago = new DateTime($datetime, $wp_tz);
1313
1314 $diff = $now->diff($ago);
1315
1316 $string = [
1317 'y' => 'year',
1318 'm' => 'month',
1319 'w' => 'week',
1320 'd' => 'day',
1321 'h' => 'hour',
1322 'i' => 'minute',
1323 's' => 'second',
1324 ];
1325
1326 foreach ($string as $k => &$v) {
1327 if (isset($diff->$k) && $diff->$k) {
1328 $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
1329 } else {
1330 unset($string[$k]);
1331 }
1332 }
1333 if (!$full) $string = array_slice($string, 0, 1);
1334 return $string ? implode(', ', $string) . ' ago' : 'just now';
1335 }
1336
1337 public function searchatlas_api_key_callback()
1338 {
1339 $current_api_key = isset(Metasync::get_option('general')['searchatlas_api_key']) ? esc_attr(Metasync::get_option('general')['searchatlas_api_key']) : '';
1340 $otto_uuid = isset(Metasync::get_option('general')['otto_pixel_uuid']) ? Metasync::get_option('general')['otto_pixel_uuid'] : '';
1341
1342 $has_api_key = !empty($current_api_key);
1343 $has_otto_uuid = !empty($otto_uuid);
1344
1345 $is_fully_connected = $this->admin_instance->is_heartbeat_connected();
1346
1347 printf('<div class="metasync-sa-connect-container">');
1348
1349 printf('<div class="metasync-sa-connect-title">');
1350 printf('One-Click Authentication');
1351 printf('</div>');
1352
1353 printf('<div class="metasync-sa-connect-description">');
1354 if ($is_fully_connected) {
1355 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()));
1356 } elseif ($has_api_key && !$has_otto_uuid) {
1357 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()));
1358 } else {
1359 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()));
1360 }
1361 printf('</div>');
1362
1363 ?>
1364 <div class="metasync-mcp-consent" style="margin-top: 15px; padding: 12px 15px; background: #f0f9ff; border-left: 3px solid #0ea5e9; border-radius: 4px;">
1365 <p style="margin: 0; color: #334155; font-size: 13px; line-height: 1.6;">
1366 <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.
1367 </p>
1368 </div>
1369 <?php
1370
1371 printf('<div class="metasync-sa-connect-buttons">');
1372
1373 printf('<button type="button" id="connect-searchatlas-btn" class="metasync-sa-connect-btn">');
1374 if ($is_fully_connected) {
1375 printf('Re-authenticate with %s', esc_html(Metasync::get_effective_plugin_name()));
1376 } elseif ($has_api_key && !$has_otto_uuid) {
1377 printf('Complete Authentication Setup');
1378 } else {
1379 printf('Connect to %s', esc_html(Metasync::get_effective_plugin_name()));
1380 }
1381 printf('</button>');
1382
1383 if ($has_api_key) {
1384 printf('<button type="button" id="reset-searchatlas-auth" class="metasync-sa-reset-btn" style="margin-left: 10px;">');
1385 printf('Disconnect Account');
1386 printf('</button>');
1387 }
1388
1389 printf('</div>');
1390
1391 printf('<div style="margin-top: 15px;">');
1392 printf('<details style="margin-top: 10px;">');
1393 printf('<summary style="cursor: pointer; color: #666; font-size: 13px;">Authentication Tips</summary>');
1394 printf('<div style="padding: 10px 0; color: #666; font-size: 13px; line-height: 1.5;">');
1395 printf('• Make sure you have a %s account before connecting<br/>', esc_html(Metasync::get_effective_plugin_name()));
1396 printf('• The authentication window will open in a popup - please allow popups<br/>');
1397 printf('• The process typically takes 15-30 seconds to complete<br/>');
1398 printf('• Your API key will be automatically filled in the field below<br/>');
1399 printf('• If you encounter issues, try disabling ad blockers temporarily<br/>');
1400 printf('• Contact <a href="mailto:%s">%s</a> if you need assistance', Metasync::SUPPORT_EMAIL, Metasync::SUPPORT_EMAIL);
1401 printf('</div>');
1402 printf('</details>');
1403 printf('</div>');
1404
1405 printf('</div>');
1406
1407 printf('<div style="margin-top: 20px;">');
1408 printf('<label for="searchatlas-api-key" style="font-weight: 600; display: block; margin-bottom: 8px;">');
1409 printf('%s API Key', esc_html(Metasync::get_effective_plugin_name()));
1410 if ($is_fully_connected) {
1411 printf('<span style="color: #46b450; margin-left: 10px; font-weight: normal;">✓ Synced</span>');
1412 } elseif ($has_api_key && !$has_otto_uuid) {
1413 printf('<span style="color: #ff8c00; margin-left: 10px; font-weight: normal;">Partial Connection (Missing %s UUID)</span>', esc_html(Metasync::get_whitelabel_otto_name()));
1414 }
1415 printf('</label>');
1416
1417 printf(
1418 '<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" />',
1419 $current_api_key
1420 );
1421
1422 printf('<p class="description" style="margin-top: 8px;">');
1423 if ($is_fully_connected) {
1424 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()));
1425 } elseif ($has_api_key && !$has_otto_uuid) {
1426 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()));
1427 } else {
1428 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.');
1429 }
1430 printf('</p>');
1431
1432 ?>
1433 <p class="description" style="margin-top: 10px; padding: 8px 12px; background: #fff9e6; border-left: 3px solid #f0b849; border-radius: 4px; font-size: 12px;">
1434 <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.
1435 </p>
1436 <?php
1437
1438 printf('</div>');
1439
1440 if( isset(Metasync::get_option('general')['searchatlas_api_key'])&&Metasync::get_option('general')['searchatlas_api_key']!=''){
1441 $timestamp = @Metasync::get_option('general')['send_auth_token_timestamp'];
1442 printf(
1443 '<p id="sendAuthTokenTimestamp" class="descriptionValue">%s (%s)</p>',
1444 esc_attr($timestamp),
1445 $this->time_elapsed_string($timestamp)
1446 );
1447
1448
1449 }
1450 }
1451
1452 public function bing_site_verification_callback()
1453 {
1454 printf(
1455 '<input type="text" id="bing_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][bing_site_verification]" value="%s" size="50" />',
1456 isset(Metasync::get_option('searchengines')['bing_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['bing_site_verification']) : ''
1457 );
1458
1459 printf(' <br> <span class="description"> Enter Bing Webmaster Tools verification code: </span> ');
1460 printf(' <a href="https://www.bing.com/webmasters/about" target="_blank">Get from here</a> <br> ');
1461 }
1462
1463 public function yandex_site_verification_callback()
1464 {
1465 printf(
1466 '<input type="text" id="yandex_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][yandex_site_verification]" value="%s" size="50" />',
1467 isset(Metasync::get_option('searchengines')['yandex_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['yandex_site_verification']) : ''
1468 );
1469
1470 printf(' <br> <span class="description"> Enter Yandex verification code: </span>');
1471 printf(' <a href="https://passport.yandex.com/auth" target="_blank">Get from here</a> <br> ');
1472 }
1473
1474 public function google_site_verification_callback()
1475 {
1476 printf(
1477 '<input type="text" id="google_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][google_site_verification]" value="%s" size="50" />',
1478 isset(Metasync::get_option('searchengines')['google_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['google_site_verification']) : ''
1479 );
1480
1481 printf(' <br> <span class="description"> Enter Google Search Console verification code: </span>');
1482 printf(' <a href="https://www.google.com/webmasters/verification" target="_blank">Get from here</a> <br> ');
1483 }
1484
1485 public function pinterest_site_verification_callback()
1486 {
1487 printf(
1488 '<input type="text" id="pinterest_site_verification" name="' . Metasync_Admin::option_key . '[searchengines][pinterest_site_verification]" value="%s" size="50" />',
1489 isset(Metasync::get_option('searchengines')['pinterest_site_verification']) ? esc_attr(Metasync::get_option('searchengines')['pinterest_site_verification']) : ''
1490 );
1491
1492 printf(' <br> <span class="description"> Enter Pinterest verification code: </span>');
1493 printf(' <a href="https://in.pinterest.com/" target="_blank">Get from here</a> <br> ');
1494 }
1495
1496 public function local_seo_person_organization_callback()
1497 {
1498 $person_organization = Metasync::get_option('localseo')['local_seo_person_organization'] ?? '';
1499 ?>
1500 <div class="metasync-type-select-wrap">
1501 <select id="local_seo_person_organization" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_person_organization]') ?>">
1502 <?php
1503 printf('<option value="Person" %s >Person</option>', selected('Person', esc_attr($person_organization)));
1504 printf('<option value="Organization" %s >Organization</option>', selected('Organization', esc_attr($person_organization)));
1505 ?>
1506 </select>
1507 </div>
1508 <?php
1509 printf(' <br> <span class="description"> Choose whether the site represents a person or an organization. </span>');
1510 }
1511
1512 public function local_seo_name_callback()
1513 {
1514 printf(
1515 '<input type="text" id="local_seo_name" name="' . Metasync_Admin::option_key . '[localseo][local_seo_name]" value="%s" size="50" />',
1516 isset(Metasync::get_option('localseo')['local_seo_name']) ? esc_attr(Metasync::get_option('localseo')['local_seo_name']) : get_bloginfo()
1517 );
1518
1519 printf(' <br> <span class="description"> Your name or company name </span>');
1520 }
1521
1522 public function local_seo_logo_callback()
1523 {
1524 $local_seo_logo = Metasync::get_option('localseo')['local_seo_logo'] ?? '';
1525
1526 printf(
1527 '<input type="hidden" id="local_seo_logo" name="' . Metasync_Admin::option_key . '[localseo][local_seo_logo]" value="%s" size="50" />',
1528 isset(Metasync::get_option('localseo')['local_seo_logo']) ? esc_attr(Metasync::get_option('localseo')['local_seo_logo']) : ''
1529 );
1530
1531 printf(' <br> <input class="button-secondary" type="button" id="logo_upload_button" value="Add or Upload File">');
1532
1533 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> ');
1534
1535 printf('<img src="%s" id="local_seo_business_logo" width="300">', wp_get_attachment_image_src($local_seo_logo, 'medium')[0] ?? '');
1536
1537 $button_type = 'hidden';
1538 if ($local_seo_logo) {
1539 $button_type = 'button';
1540 }
1541 printf('<input type="%s" class="button-secondary" id="local_seo_logo_close_btn" value="X">', $button_type);
1542 }
1543
1544 public function local_seo_url_callback()
1545 {
1546 printf(
1547 '<input type="text" id="local_seo_url" name="' . Metasync_Admin::option_key . '[localseo][local_seo_url]" value="%s" size="50" />',
1548 isset(Metasync::get_option('localseo')['local_seo_url']) ? esc_attr(Metasync::get_option('localseo')['local_seo_url']) : home_url()
1549 );
1550
1551 printf(' <br> <span class="description"> URL of the item. </span>');
1552 }
1553
1554 public function local_seo_email_callback()
1555 {
1556 printf(
1557 '<input type="text" id="local_seo_email" name="' . Metasync_Admin::option_key . '[localseo][local_seo_email]" value="%s" size="50" />',
1558 isset(Metasync::get_option('localseo')['local_seo_email']) ? esc_attr(Metasync::get_option('localseo')['local_seo_email']) : ''
1559 );
1560
1561 printf(' <br> <span class="description"> Search engines display your email address. </span>');
1562 }
1563
1564 public function local_seo_phone_callback()
1565 {
1566 printf(
1567 '<input type="text" id="local_seo_phone" name="' . Metasync_Admin::option_key . '[localseo][local_seo_phone]" value="%s" size="50" />',
1568 isset(Metasync::get_option('localseo')['local_seo_phone']) ? esc_attr(Metasync::get_option('localseo')['local_seo_phone']) : ''
1569 );
1570
1571 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1572 }
1573
1574 public function local_seo_address_callback()
1575 {
1576 printf(
1577 '<input type="text" id="local_seo_address_street" name="' . Metasync_Admin::option_key . '[localseo][address][street]" value="%s" size="50" placeholder="Street Address"/> <br>',
1578 isset(Metasync::get_option('localseo')['address']['street']) ? esc_attr(Metasync::get_option('localseo')['address']['street']) : ''
1579 );
1580
1581 printf(
1582 '<input type="text" id="local_seo_address_locality" name="' . Metasync_Admin::option_key . '[localseo][address][locality]" value="%s" size="50" placeholder="Locality"/> <br>',
1583 isset(Metasync::get_option('localseo')['address']['locality']) ? esc_attr(Metasync::get_option('localseo')['address']['locality']) : ''
1584 );
1585
1586 printf(
1587 '<input type="text" id="local_seo_address_region" name="' . Metasync_Admin::option_key . '[localseo][address][region]" value="%s" size="50" placeholder="Region"/> <br>',
1588 isset(Metasync::get_option('localseo')['address']['region']) ? esc_attr(Metasync::get_option('localseo')['address']['region']) : ''
1589 );
1590
1591 printf(
1592 '<input type="text" id="local_seo_address_postalcode" name="' . Metasync_Admin::option_key . '[localseo][address][postalcode]" value="%s" size="50" placeholder="Postal Code"/> <br>',
1593 isset(Metasync::get_option('localseo')['address']['postalcode']) ? esc_attr(Metasync::get_option('localseo')['address']['postalcode']) : ''
1594 );
1595
1596 printf(
1597 '<input type="text" id="local_seo_address_country" name="' . Metasync_Admin::option_key . '[localseo][address][country]" value="%s" size="50" placeholder="Country"/> <br>',
1598 isset(Metasync::get_option('localseo')['address']['country']) ? esc_attr(Metasync::get_option('localseo')['address']['country']) : ''
1599 );
1600 }
1601
1602 public function local_seo_business_type_callback()
1603 {
1604 $types = self::get_business_types();
1605 sort($types);
1606
1607 $business_type = Metasync::get_option('localseo')['local_seo_business_type'] ?? '';
1608
1609 ?>
1610 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_business_type]') ?>">
1611 <option value='0'>Select Business Type</option>
1612 <?php
1613 foreach ($types as $type) {
1614 printf('<option value="%s" %s >%s</option>', $type, selected($type, esc_attr($business_type)), $type);
1615 }
1616 ?>
1617 </select>
1618 <?php
1619 }
1620
1621 public function local_seo_opening_hours_callback()
1622 {
1623 $days_name = ['Monday', 'Tuseday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
1624 $days = isset(Metasync::get_option('localseo')['days']) ? Metasync::get_option('localseo')['days'] : '';
1625 $times = isset(Metasync::get_option('localseo')['times']) ? Metasync::get_option('localseo')['times'] : '';
1626
1627 ?>
1628 <ul id="daysTime">
1629 <?php
1630 $opening_days = [];
1631 if ($days && $times) {
1632 $opening_days = array_combine($days, $times);
1633 }
1634 foreach ($opening_days as $day_name => $day_time) {
1635 ?>
1636 <li>
1637 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1638 <?php
1639 foreach ($days_name as $name) {
1640 printf('<option value="%s" %s >%s</option>', $name, selected(esc_attr($name), esc_attr($day_name)), esc_attr($name));
1641 }
1642 ?>
1643 </select>
1644 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="<?php echo esc_attr($day_time) ?>">
1645 <button id="timeDelete">Delete</button>
1646 </li>
1647 <?php } ?>
1648 <?php if (empty($opening_days)) { ?>
1649 <li>
1650 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][days][]') ?>">
1651 <?php
1652 foreach ($days_name as $name) {
1653 printf('<option value="%s" >%s</option>', esc_attr($name), esc_attr($name));
1654 }
1655 ?>
1656 </select>
1657 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][times][]') ?>" value="">
1658 <button id="timeDelete">Delete</button>
1659 </li>
1660 <?php } ?>
1661 </ul>
1662 <?php
1663
1664 printf(' <input type="hidden" id="days_time_count" value="%s"/>', count($opening_days));
1665 printf(' <input class="button-secondary" type="button" id="addNewTime" value="Add Time">');
1666 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>');
1667 }
1668
1669 public function local_seo_phone_numbers_callback()
1670 {
1671 $number_types = ['Customer Service', 'Technical Support', 'Billing Support', 'Bill Payment', 'Sales', 'Reservations', 'Credit Card Support', 'Emergency', 'Baggage Tracking', 'Roadside Assistance', 'Package Tracking'];
1672 $types = isset(Metasync::get_option('localseo')['phonetype']) ? Metasync::get_option('localseo')['phonetype'] : '';
1673 $numbers = isset(Metasync::get_option('localseo')['phonenumber']) ? Metasync::get_option('localseo')['phonenumber'] : '';
1674
1675 ?>
1676
1677 <ul id="phone-numbers">
1678 <?php
1679 $phone_numbers = [];
1680 if ($types && $numbers) {
1681 $phone_numbers = array_combine($types, $numbers);
1682 }
1683 foreach ($phone_numbers as $phone_type => $phone_number) {
1684 ?>
1685 <li>
1686 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1687 <?php
1688 foreach ($number_types as $type) {
1689 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($phone_type)), esc_attr($type));
1690 }
1691 ?>
1692 </select>
1693 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="<?php echo esc_attr($phone_number) ?>">
1694 <button id="number-delete">Delete</button>
1695 </li>
1696 <?php } ?>
1697 <?php if (empty($phone_numbers)) { ?>
1698 <li>
1699 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonetype][]') ?>">
1700 <?php
1701 foreach ($number_types as $type) {
1702 printf('<option value="%s" >%s</option>', esc_attr($type), esc_attr($type));
1703 }
1704 ?>
1705 </select>
1706 <input type="text" name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][phonenumber][]') ?>" value="">
1707 <button id="number-delete">Delete</button>
1708 </li>
1709 <?php } ?>
1710 </ul>
1711 <?php
1712
1713 printf(' <input type="hidden" id="phone_number_count" value="%s"/>', count($phone_numbers));
1714 printf(' <input class="button-secondary" type="button" id="addNewNumber" value="Add Number">');
1715 printf(' <br> <span class="description"> Search engines may prominently display your contact phone number for mobile users. </span>');
1716 }
1717
1718 public function local_seo_price_range_callback()
1719 {
1720 printf(
1721 '<input type="text" id="local_seo_price_range" name="' . Metasync_Admin::option_key . '[localseo][local_seo_price_range]" value="%s" size="50" />',
1722 isset(Metasync::get_option('localseo')['local_seo_price_range']) ? esc_attr(Metasync::get_option('localseo')['local_seo_price_range']) : ''
1723 );
1724 printf(' <br> <span class="description"> The price range of the business, for example $$$. </span>');
1725 }
1726
1727 public function local_seo_about_page_callback()
1728 {
1729 ?>
1730 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_about_page]') ?>">
1731 <option value='0'>Select About Page</option>
1732 <?php
1733 $about_page = Metasync::get_option('localseo')['local_seo_about_page'] ?? '';
1734 $pages = get_pages();
1735 foreach ($pages as $page) {
1736 printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($about_page)), $page->post_title);
1737 }
1738 ?>
1739 </select>
1740 <?php
1741 printf(' <br> <span class="description"> Search engines tag your about us page. </span>');
1742 }
1743
1744 public function local_seo_contact_page_callback()
1745 {
1746 ?>
1747 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[localseo][local_seo_contact_page]') ?>">
1748 <option value='0'>Select Contact Page</option>
1749 <?php
1750 $contact_page = Metasync::get_option('localseo')['local_seo_contact_page'] ?? '';
1751 $pages = get_pages();
1752 foreach ($pages as $page) {
1753 printf('<option value="%s" %s >%s</option>', $page->ID, selected($page->ID, esc_attr($contact_page)), $page->post_title);
1754 }
1755 ?>
1756 </select>
1757 <?php
1758 printf(' <br> <span class="description"> Search engines tag your contact page. </span>');
1759 }
1760
1761 public function local_seo_map_key_callback()
1762 {
1763 printf(
1764 '<input type="text" id="local_seo_map_key" name="' . Metasync_Admin::option_key . '[localseo][local_seo_map_key]" value="%s" size="50" />',
1765 isset(Metasync::get_option('localseo')['local_seo_map_key']) ? esc_attr(Metasync::get_option('localseo')['local_seo_map_key']) : ''
1766 );
1767
1768 printf(' <br> <span class="description"> An API Key is required to display embedded Google Maps on your site. </span>');
1769 }
1770
1771 public function local_seo_geo_coordinates_callback()
1772 {
1773 printf(
1774 '<input type="text" id="local_seo_geo_coordinates" name="' . Metasync_Admin::option_key . '[localseo][local_seo_geo_coordinates]" value="%s" size="50" />',
1775 isset(Metasync::get_option('localseo')['local_seo_geo_coordinates']) ? esc_attr(Metasync::get_option('localseo')['local_seo_geo_coordinates']) : ''
1776 );
1777
1778 printf(' <br> <span class="description"> Latitude and longitude values separated by comma. </span>');
1779 }
1780
1781 public function header_snippets_callback()
1782 {
1783 printf(
1784 '<textarea class="wide-text" id="header_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][header_snippet]" >%s</textarea>',
1785 isset(Metasync::get_option('codesnippets')['header_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['header_snippet']) : ''
1786 );
1787 }
1788
1789 public function footer_snippets_callback()
1790 {
1791 printf(
1792 '<textarea class="wide-text" id="footer_snippets" rows="8" name="' . Metasync_Admin::option_key . '[codesnippets][footer_snippet]" >%s</textarea>',
1793 isset(Metasync::get_option('codesnippets')['footer_snippet']) ? esc_attr(Metasync::get_option('codesnippets')['footer_snippet']) : ''
1794 );
1795 }
1796
1797 public function no_index_posts_callback()
1798 {
1799 printf(
1800 '<input type="checkbox" id="no_index_posts" name="' . Metasync_Admin::option_key . '[optimal_settings][no_index_posts]" value="true" %s />',
1801 isset(Metasync::get_option('optimal_settings')['no_index_posts']) && Metasync::get_option('optimal_settings')['no_index_posts'] == 'true' ? 'checked' : ''
1802 );
1803
1804 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>');
1805 }
1806
1807 public function no_follow_links_callback()
1808 {
1809 printf(
1810 '<input type="checkbox" id="no_follow_links" name="' . Metasync_Admin::option_key . '[optimal_settings][no_follow_links]" value="true" %s />',
1811 isset(Metasync::get_option('optimal_settings')['no_follow_links']) && Metasync::get_option('optimal_settings')['no_follow_links'] == 'true' ? 'checked' : ''
1812 );
1813
1814 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>');
1815 }
1816
1817 public function open_external_links_callback()
1818 {
1819 printf(
1820 '<input type="checkbox" id="open_external_links" name="' . Metasync_Admin::option_key . '[optimal_settings][open_external_links]" value="true" %s />',
1821 isset(Metasync::get_option('optimal_settings')['open_external_links']) && Metasync::get_option('optimal_settings')['open_external_links'] == 'true' ? 'checked' : ''
1822 );
1823
1824 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>');
1825 }
1826
1827 public function add_alt_image_tags_callback()
1828 {
1829 printf(
1830 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_alt_image_tags]" value="true" %s />',
1831 isset(Metasync::get_option('optimal_settings')['add_alt_image_tags']) && Metasync::get_option('optimal_settings')['add_alt_image_tags'] == 'true' ? 'checked' : ''
1832 );
1833
1834 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>');
1835 }
1836
1837 public function add_title_image_tags_callback()
1838 {
1839 printf(
1840 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[optimal_settings][add_title_image_tags]" value="true" %s />',
1841 isset(Metasync::get_option('optimal_settings')['add_title_image_tags']) && Metasync::get_option('optimal_settings')['add_title_image_tags'] == 'true' ? 'checked' : ''
1842 );
1843
1844 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>');
1845 }
1846
1847 public function site_type_callback()
1848 {
1849
1850 $site_type = Metasync::get_option('optimal_settings')['site_info']['type'] ?? '';
1851
1852 $types = [
1853 ['name' => 'Personal Blog', 'value' => 'blog'],
1854 ['name' => 'Community Blog/News Site', 'value' => 'news'],
1855 ['name' => 'Personal Portfolio', 'value' => 'portfolio'],
1856 ['name' => 'Small Business Site', 'value' => 'business'],
1857 ['name' => 'Webshop', 'value' => 'webshop'],
1858 ['name' => 'Other Personal Website', 'value' => 'otherpersonal'],
1859 ['name' => 'Other Business Website', 'value' => 'otherbusiness'],
1860 ];
1861
1862 ?>
1863 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][type]') ?>" id="site_info_type">
1864 <?php
1865 foreach ($types as $type) {
1866 printf('<option value="%s" %s >%s</option>', esc_attr($type['value']), selected(esc_attr($type['value']), esc_attr($site_type)), ($type['name']));
1867 }
1868 ?>
1869 </select>
1870 <?php
1871
1872 }
1873
1874 public function site_business_type_callback()
1875 {
1876
1877 $business_type = Metasync::get_option('optimal_settings')['site_info']['business_type'] ?? '';
1878
1879 $types = self::get_business_types();
1880 sort($types);
1881
1882 ?>
1883 <select name="<?php echo esc_attr(Metasync_Admin::option_key . '[optimal_settings][site_info][business_type]') ?>">
1884 <option value='0'>Select Business Type</option>
1885 <?php
1886 foreach ($types as $type) {
1887 printf('<option value="%s" %s >%s</option>', esc_attr($type), selected(esc_attr($type), esc_attr($business_type)), esc_attr($type));
1888 }
1889 ?>
1890 </select>
1891 <?php
1892 }
1893
1894 public function site_company_name_callback()
1895 {
1896
1897 $company_name = Metasync::get_option('optimal_settings')['site_info']['company_name'] ?? get_bloginfo('name');
1898
1899 printf(
1900 '<input type="text" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][company_name]" value="%s" size="50" />',
1901 $company_name ? $company_name : get_bloginfo('name')
1902 );
1903 }
1904
1905 public function site_google_logo_callback()
1906 {
1907
1908 $google_logo = Metasync::get_option('optimal_settings')['site_info']['google_logo'] ?? '';
1909
1910 printf(
1911 '<input type="hidden" id="site_google_logo" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][google_logo]" value="%s" size="50" />',
1912 $google_logo
1913 );
1914
1915 printf(' <br> <input class="button-secondary" type="button" id="google_logo_btn" value="Add or Upload File">');
1916 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> ');
1917 printf('<img src="%s" id="site_google_logo_img" width="300">', wp_get_attachment_image_src($google_logo, 'medium')[0] ?? '');
1918
1919 $button_type = 'hidden';
1920 if ($google_logo) {
1921 $button_type = 'button';
1922 }
1923 printf('<input type="%s" class="button-secondary" id="site_google_logo_close_btn" value="X">', $button_type);
1924 }
1925
1926 public function site_social_share_image_callback()
1927 {
1928
1929 $social_share_image = Metasync::get_option('optimal_settings')['site_info']['social_share_image'] ?? '';
1930
1931 printf(
1932 '<input type="hidden" id="site_social_share_image" name="' . Metasync_Admin::option_key . '[optimal_settings][site_info][social_share_image]" value="%s" size="50" />',
1933 $social_share_image
1934 );
1935
1936 printf(' <br> <input class="button-secondary" type="button" id="social_share_image_btn" value="Add or Upload File">');
1937 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> ');
1938 printf('<img src="%s" id="site_social_share_img" width="300">', wp_get_attachment_image_src($social_share_image, 'medium')[0] ?? '');
1939
1940 $button_type = 'hidden';
1941 if ($social_share_image) {
1942 $button_type = 'button';
1943 }
1944 printf('<input type="%s" class="button-secondary" id="site_social_image_close_btn" value="X">', $button_type);
1945 }
1946
1947 public function common_robot_meta_tags_callback()
1948 {
1949 $common_robots_meta = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
1950
1951 ?>
1952 <ul class="checkbox-list">
1953 <li>
1954 <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']) : '' ?>>
1955 <label for="robots_common1">Index </br>
1956 <span class="description">
1957 <span>Search engines to index and show these pages in the search results.</span>
1958 </span>
1959 </label>
1960 </li>
1961 <li>
1962 <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']) : '' ?>>
1963 <label for="robots_common2">No Index </br>
1964 <span class="description">
1965 <span>Search engines not indexed and displayed this pages in search engine results</span>
1966 </span>
1967 </label>
1968 </li>
1969 <li>
1970 <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']) : '' ?>>
1971 <label for="robots_common3">No Follow </br>
1972 <span class="description">
1973 <span>Search engines not follow the links on the pages</span>
1974 </span>
1975 </label>
1976 </li>
1977 <li>
1978 <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']) : '' ?>>
1979 <label for="robots_common4">No Archive </br>
1980 <span class="description">
1981 <span>Search engines not showing Cached links for pages</span>
1982 </span>
1983 </label>
1984 </li>
1985 <li>
1986 <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']) : '' ?>>
1987 <label for="robots_common5">No Image Index </br>
1988 <span class="description">
1989 <span>If you do not want to apear your pages as the referring page for images that appear in image search results</span>
1990 </span>
1991 </label>
1992 </li>
1993 <li>
1994 <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']) : '' ?>>
1995 <label for="robots_common6">No Snippet </br>
1996 <span class="description">
1997 <span>Search engines not snippet to show in the search results</span>
1998 </span>
1999 </label>
2000 </li>
2001 </ul>
2002 <?php
2003 }
2004
2005 /**
2006 * @deprecated Use common_robot_meta_tags_callback() instead
2007 */
2008 public function common_robot_mata_tags_callback()
2009 {
2010 return $this->common_robot_meta_tags_callback();
2011 }
2012
2013 public function advance_robot_meta_tags_callback()
2014 {
2015 $advance_robots_meta = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
2016
2017 $snippet_advance_robots_enable = $advance_robots_meta['max-snippet']['enable'] ?? '';
2018 $snippet_advance_robots_length = $advance_robots_meta['max-snippet']['length'] ?? '-1';
2019 $video_advance_robots_enable = $advance_robots_meta['max-video-preview']['enable'] ?? '';
2020 $video_advance_robots_length = $advance_robots_meta['max-video-preview']['length'] ?? '-1';
2021 $image_advance_robots_enable = $advance_robots_meta['max-image-preview']['enable'] ?? '';
2022 $image_advance_robots_length = $advance_robots_meta['max-image-preview']['length'] ?? '';
2023
2024 ?>
2025 <ul class="checkbox-list">
2026 <li>
2027 <label for="advanced_robots_snippet">
2028 <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)) ?>>
2029 Snippet </br>
2030 <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>
2031 <span class="description">
2032 <span>Add maximum text-length, in characters, of a snippet for your page.</span>
2033 </span>
2034 </label>
2035 </li>
2036 <li>
2037 <label for="advanced_robots_video">
2038 <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)) ?>>
2039 Video Preview </br>
2040 <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>
2041 <span class="description">
2042 <span>Add maximum duration in seconds of an animated video preview.</span>
2043 </span>
2044 </label>
2045 </li>
2046 <li>
2047 <label for="advanced_robots_image">
2048 <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)); ?>>
2049 Image Preview </br>
2050 <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">
2051 <option value="large" <?php selected('large', esc_attr($image_advance_robots_length)) ?>>Large</option>
2052 <option value="standard" <?php selected('standard', esc_attr($image_advance_robots_length)) ?>>Standard</option>
2053 <option value="none" <?php selected('none', esc_attr($image_advance_robots_length)) ?>>None</option>
2054 </select>
2055 </br>
2056 <span class="description">
2057 <span>Add maximum size of image preview to show the images on this page.</span>
2058 </span>
2059 </label>
2060 </li>
2061 </ul>
2062 <?php
2063 }
2064
2065 /**
2066 * @deprecated Use advance_robot_meta_tags_callback() instead
2067 */
2068 public function advance_robot_mata_tags_callback()
2069 {
2070 return $this->advance_robot_meta_tags_callback();
2071 }
2072
2073 public function global_twitter_card_type_callback()
2074 {
2075 $twitter_card_type = Metasync::get_option('twitter_card_type') ?? '';
2076 ?>
2077
2078 <select class="input-length" name="<?php echo esc_attr(Metasync_Admin::option_key . '[twitter_card_type]') ?>" id="twitter_card_type">
2079 <option value="summary_large_image" <?php selected('summary_large_image', esc_attr($twitter_card_type)) ?>>Summary Large Image</option>
2080 <option value="summary_card" <?php selected('summary_card', esc_attr($twitter_card_type)) ?>>Summary Card</option>
2081 </select>
2082
2083 <?php
2084 }
2085
2086 public function global_open_graph_meta_callback()
2087 {
2088 printf(
2089 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][open_graph_meta_tags]" value="true" %s />',
2090 isset(Metasync::get_option('common_meta_settings')['open_graph_meta_tags']) && Metasync::get_option('common_meta_settings')['open_graph_meta_tags'] == 'true' ? 'checked' : ''
2091 );
2092 printf(' <br> <span class="description"> Automatically add the Open Graph meta tags in a page or post.</span>');
2093 }
2094
2095 public function global_facebook_meta_callback()
2096 {
2097 printf(
2098 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][facebook_meta_tags]" value="true" %s />',
2099 isset(Metasync::get_option('common_meta_settings')['facebook_meta_tags']) && Metasync::get_option('common_meta_settings')['facebook_meta_tags'] == 'true' ? 'checked' : ''
2100 );
2101 printf(' <br> <span class="description"> Automatically add the Facebook meta tags in a page or post.</span>');
2102 }
2103
2104 public function global_twitter_meta_callback()
2105 {
2106 printf(
2107 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_meta_tags]" value="true" %s />',
2108 isset(Metasync::get_option('common_meta_settings')['twitter_meta_tags']) && Metasync::get_option('common_meta_settings')['twitter_meta_tags'] == 'true' ? 'checked' : ''
2109 );
2110 printf(' <br> <span class="description"> Automatically add the Twitter meta tags in a page or post.</span>');
2111 }
2112
2113 public function og_image_dimensions_callback()
2114 {
2115 $val = Metasync::get_option('common_meta_settings')['og_image_dimensions'] ?? 'true';
2116 printf(
2117 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][og_image_dimensions]" value="true" %s />',
2118 $val === 'true' ? 'checked' : ''
2119 );
2120 printf('<span class="description"> Output og:image:width, og:image:height, and og:image:type when OG image metadata is available.</span>');
2121 }
2122
2123 public function article_timestamps_callback()
2124 {
2125 $val = Metasync::get_option('common_meta_settings')['article_timestamps'] ?? 'true';
2126 printf(
2127 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_timestamps]" value="true" %s />',
2128 $val === 'true' ? 'checked' : ''
2129 );
2130 printf('<span class="description"> Output article:published_time and article:modified_time from the post publish and modified dates (ISO 8601).</span>');
2131 }
2132
2133 public function article_author_callback()
2134 {
2135 $val = Metasync::get_option('common_meta_settings')['article_author'] ?? 'true';
2136 printf(
2137 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_author]" value="true" %s />',
2138 $val === 'true' ? 'checked' : ''
2139 );
2140 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>');
2141 }
2142
2143 public function article_section_callback()
2144 {
2145 $val = Metasync::get_option('common_meta_settings')['article_section'] ?? 'true';
2146 printf(
2147 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_section]" value="true" %s />',
2148 $val === 'true' ? 'checked' : ''
2149 );
2150 printf('<span class="description"> Output article:section from the primary category (falls back to first category).</span>');
2151 }
2152
2153 public function article_tags_callback()
2154 {
2155 $val = Metasync::get_option('common_meta_settings')['article_tags'] ?? 'true';
2156 printf(
2157 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][article_tags]" value="true" %s />',
2158 $val === 'true' ? 'checked' : ''
2159 );
2160 printf('<span class="description"> Output one article:tag meta tag per WordPress post tag.</span>');
2161 }
2162
2163 public function twitter_image_alt_callback()
2164 {
2165 $val = Metasync::get_option('common_meta_settings')['twitter_image_alt'] ?? 'true';
2166 printf(
2167 '<input type="checkbox" name="' . Metasync_Admin::option_key . '[common_meta_settings][twitter_image_alt]" value="true" %s />',
2168 $val === 'true' ? 'checked' : ''
2169 );
2170 printf('<span class="description"> Output twitter:image:alt using the stored alt text or the OG image attachment alt attribute.</span>');
2171 }
2172
2173 public function facebook_page_url_callback()
2174 {
2175 $facebook_page_url = Metasync::get_option('social_meta')['facebook_page_url'] ?? '';
2176 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_page_url]" value="%s" size="50" />', esc_attr($facebook_page_url));
2177 printf('<br><span class="description"> Enter your Facebook page URL. eg: <code>https://www.facebook.com/MetaSync/</code> </span>');
2178 }
2179
2180 public function facebook_authorship_callback()
2181 {
2182 $facebook_authorship = Metasync::get_option('social_meta')['facebook_authorship'] ?? '';
2183 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_authorship]" value="%s" size="50" />', esc_attr($facebook_authorship));
2184 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>');
2185 }
2186
2187 public function facebook_admin_callback()
2188 {
2189 $facebook_admin = Metasync::get_option('social_meta')['facebook_admin'] ?? '';
2190 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_admin]" value="%s" size="50" />', esc_attr($facebook_admin));
2191 printf(' <br> <span class="description"> Enter numeric user ID of Facebook. </span>');
2192 }
2193
2194 public function facebook_app_callback()
2195 {
2196 $facebook_app = Metasync::get_option('social_meta')['facebook_app'] ?? '';
2197 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_app]" value="%s" size="50" />', esc_attr($facebook_app));
2198 printf(' <br> <span class="description"> Enter numeric app ID of Facebook </span>');
2199 }
2200
2201 public function facebook_secret_callback()
2202 {
2203 $facebook_secret = Metasync::get_option('social_meta')['facebook_secret'] ?? '';
2204 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][facebook_secret]" value="%s" size="50" />', esc_attr($facebook_secret));
2205 printf(' <br> <span class="description"> Enter alphanumeric access token from Facebook. </span>');
2206 }
2207
2208 public function twitter_username_callback()
2209 {
2210 $twitter_username = Metasync::get_option('social_meta')['twitter_username'] ?? '';
2211 printf('<input type="text" name="' . Metasync_Admin::option_key . '[social_meta][twitter_username]" value="%s" size="50" />', esc_attr($twitter_username));
2212 printf(' <br> <span class="description"> Twitter username of the author to add <code>twitter:creator</code> tag to post. eg: <code>MetaSync</code> </span>');
2213 }
2214
2215 public static function get_business_types()
2216 {
2217 $business_type = [
2218 'Airline',
2219 'Consortium',
2220 'Corporation',
2221 'Educational Organization',
2222 'College Or University',
2223 'Elementary School',
2224 'High School',
2225 'Middle School',
2226 'Preschool',
2227 'School',
2228 'Funding Scheme',
2229 'Government Organization',
2230 'Library System',
2231 'Local Business',
2232 'Animal Shelter',
2233 'Archive Organization',
2234 'Automotive Business',
2235 'Auto Body Shop',
2236 'Auto Dealer',
2237 'Auto Parts Store',
2238 'Auto Rental',
2239 'Auto Repair',
2240 'Auto Wash',
2241 'Gas Station',
2242 'Motorcycle Dealer',
2243 'Motorcycle Repair',
2244 'Child Care',
2245 'Dry Cleaning Or Laundry',
2246 'Emergency Service',
2247 'Fire Station',
2248 'Hospital',
2249 'Police Station',
2250 'Employment Agency',
2251 'Entertainment Business',
2252 'Adult Entertainment',
2253 'Amusement Park',
2254 'Art Gallery',
2255 'Casino',
2256 'Comedy Club',
2257 'Movie Theater',
2258 'Night Club',
2259 'Financial Service',
2260 'Accounting Service',
2261 'Automated Teller',
2262 'Bank Or CreditUnion',
2263 'Insurance Agency',
2264 'Food Establishment',
2265 'Bakery',
2266 'Bar Or Pub',
2267 'Brewery',
2268 'Cafe Or CoffeeShop',
2269 'Distillery',
2270 'Fast Food Restaurant',
2271 'IceCream Shop',
2272 'Restaurant',
2273 'Winery',
2274 'Government Office',
2275 'Post Office',
2276 'Health And Beauty Business',
2277 'Beauty Salon',
2278 'Day Spa',
2279 'Hair Salon',
2280 'Health Club',
2281 'Nail Salon',
2282 'Tattoo Parlor',
2283 'Home And Construction Business',
2284 'Electrician',
2285 'General Contractor',
2286 'HVAC Business',
2287 'House Painter',
2288 'Locksmith',
2289 'Moving Company',
2290 'Plumber',
2291 'Roofing Contractor',
2292 'Internet Cafe',
2293 'Legal Service',
2294 'Attorney',
2295 'Notary',
2296 'Library',
2297 'Lodging Business',
2298 'Bed And Breakfast',
2299 'Campground',
2300 'Hostel',
2301 'Hotel',
2302 'Motel',
2303 'Resort',
2304 'Ski Resort',
2305 'Medical Business',
2306 'Community Health',
2307 'Dentist',
2308 'Dermatology',
2309 'Diet Nutrition',
2310 'Emergency',
2311 'Geriatric',
2312 'Gynecologic',
2313 'Medical Clinic',
2314 'Optician',
2315 'Pharmacy',
2316 'Physician',
2317 'Professional Service',
2318 'Radio Station',
2319 'Real Estate Agent',
2320 'Recycling Center',
2321 'Self Storage',
2322 'Shopping Center',
2323 'Sports Activity Location',
2324 'Bowling Alley',
2325 'Exercise Gym',
2326 'Golf Course',
2327 'Public Swimming Pool',
2328 'Ski Resort',
2329 'Sports Club',
2330 'Stadium Or Arena',
2331 'Tennis Complex',
2332 'Store',
2333 'Bike Store',
2334 'Book Store',
2335 'Clothing Store',
2336 'Computer Store',
2337 'Convenience Store',
2338 'Department Store',
2339 'Electronics Store',
2340 'Florist',
2341 'Furniture Store',
2342 'Garden Store',
2343 'Grocery Store',
2344 'Hardware Store',
2345 'Hobby Shop',
2346 'Home Goods Store',
2347 'Jewelry Store',
2348 'Liquor Store',
2349 'Mens Clothing Store',
2350 'Mobile Phone Store',
2351 'Movie Rental Store',
2352 'Music Store',
2353 'Office Equipment Store',
2354 'Outlet Store',
2355 'Pawn Shop',
2356 'Pet Store',
2357 'Shoe Store',
2358 'Sporting GoodsStore',
2359 'Tire Shop',
2360 'Toy Store',
2361 'Wholesale Store',
2362 'Television Station',
2363 'Tourist Information Center',
2364 'Travel Agency',
2365 'Tree Services',
2366 'Medical Organization',
2367 'Diagnostic Lab',
2368 'Veterinary Care',
2369 'NGO',
2370 'News Media Organization',
2371 'Performing Group',
2372 'Dance Group',
2373 'Music Group',
2374 'Theater Group',
2375 'Project',
2376 'Funding Agency',
2377 'Research Project',
2378 'Sports Organization',
2379 'Sports Team',
2380 'Workers Union',
2381 ];
2382
2383 return $business_type;
2384 }
2385
2386 /**
2387 * Render breadcrumbs settings section
2388 */
2389 public function render_breadcrumbs_section() {
2390 $options = Metasync::get_option('breadcrumbs', array());
2391
2392 $defaults = array(
2393 'enabled' => true,
2394 'separator' => '&raquo;',
2395 'home_label' => 'Home',
2396 'home_url' => '',
2397 'show_current_page' => true,
2398 'prefix_text' => '',
2399 'archive_label_format' => '{name}',
2400 'disable_schema' => false,
2401 );
2402
2403 $options = wp_parse_args($options, $defaults);
2404 ?>
2405 <div style="background: var(--dashboard-card-bg); padding: 20px; border-radius: 8px;">
2406
2407 <!-- Enable/Disable -->
2408 <div style="margin-bottom: 24px;">
2409 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2410 <input type="checkbox"
2411 name="metasync_options[breadcrumbs][enabled]"
2412 value="1"
2413 <?php checked($options['enabled'], 1); ?>
2414 style="width: 18px; height: 18px; cursor: pointer;">
2415 <span style="font-weight: 500; color: var(--dashboard-text);">Enable breadcrumbs globally</span>
2416 </label>
2417 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2418 Enable or disable breadcrumb navigation on your site
2419 </p>
2420 </div>
2421
2422 <!-- Disable Schema Markup -->
2423 <div style="margin-bottom: 24px;">
2424 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2425 <input type="checkbox"
2426 name="metasync_options[breadcrumbs][disable_schema]"
2427 value="1"
2428 <?php checked($options['disable_schema'], 1); ?>
2429 style="width: 18px; height: 18px; cursor: pointer;">
2430 <span style="font-weight: 500; color: var(--dashboard-text);">Disable breadcrumb schema markup (JSON-LD)</span>
2431 </label>
2432 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2433 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.
2434 </p>
2435 </div>
2436
2437 <!-- Separator Character -->
2438 <div style="margin-bottom: 24px;">
2439 <label for="breadcrumb_separator" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2440 Separator Character
2441 </label>
2442 <div style="display: flex; gap: 12px; flex-wrap: wrap;">
2443 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2444 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="»" <?php checked($options['separator'], '»'); ?>>
2445 <span style="color: var(--dashboard-text);">»</span>
2446 </label>
2447 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2448 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="/" <?php checked($options['separator'], '/'); ?>>
2449 <span style="color: var(--dashboard-text);">/</span>
2450 </label>
2451 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2452 <input type="radio" name="metasync_options[breadcrumbs][separator]" value=">" <?php checked($options['separator'], '>'); ?>>
2453 <span style="color: var(--dashboard-text);">></span>
2454 </label>
2455 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
2456 <input type="radio" name="metasync_options[breadcrumbs][separator]" value="·" <?php checked($options['separator'], '·'); ?>>
2457 <span style="color: var(--dashboard-text);">·</span>
2458 </label>
2459 </div>
2460 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2461 Choose how to separate breadcrumb items
2462 </p>
2463 </div>
2464
2465 <!-- Home Label -->
2466 <div style="margin-bottom: 24px;">
2467 <label for="breadcrumb_home_label" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2468 Home Label
2469 </label>
2470 <input type="text"
2471 id="breadcrumb_home_label"
2472 name="metasync_options[breadcrumbs][home_label]"
2473 value="<?php echo esc_attr($options['home_label']); ?>"
2474 placeholder="Home"
2475 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;">
2476 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2477 Label for the home/root breadcrumb item
2478 </p>
2479 </div>
2480
2481 <!-- Home URL -->
2482 <div style="margin-bottom: 24px;">
2483 <label for="breadcrumb_home_url" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2484 Home URL
2485 </label>
2486 <input type="url"
2487 id="breadcrumb_home_url"
2488 name="metasync_options[breadcrumbs][home_url]"
2489 value="<?php echo esc_attr($options['home_url']); ?>"
2490 placeholder="<?php echo esc_attr(home_url('/')); ?>"
2491 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;">
2492 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2493 Leave empty to use your site home URL
2494 </p>
2495 </div>
2496
2497 <!-- Show Current Page -->
2498 <div style="margin-bottom: 24px;">
2499 <label style="display: flex; align-items: center; gap: 12px; cursor: pointer;">
2500 <input type="checkbox"
2501 name="metasync_options[breadcrumbs][show_current_page]"
2502 value="1"
2503 <?php checked($options['show_current_page'], 1); ?>
2504 style="width: 18px; height: 18px; cursor: pointer;">
2505 <span style="font-weight: 500; color: var(--dashboard-text);">Show current page</span>
2506 </label>
2507 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2508 Include the current page as the last (unlinked) breadcrumb item
2509 </p>
2510 </div>
2511
2512 <!-- Prefix Text -->
2513 <div style="margin-bottom: 24px;">
2514 <label for="breadcrumb_prefix" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2515 Prefix Text
2516 </label>
2517 <input type="text"
2518 id="breadcrumb_prefix"
2519 name="metasync_options[breadcrumbs][prefix_text]"
2520 value="<?php echo esc_attr($options['prefix_text']); ?>"
2521 placeholder='e.g., "You are here:"'
2522 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;">
2523 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2524 Optional text to display before the breadcrumbs (leave empty for none)
2525 </p>
2526 </div>
2527
2528 <!-- Archive Label Format -->
2529 <div style="margin-bottom: 24px;">
2530 <label for="breadcrumb_archive_format" style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
2531 Archive Label Format
2532 </label>
2533 <input type="text"
2534 id="breadcrumb_archive_format"
2535 name="metasync_options[breadcrumbs][archive_label_format]"
2536 value="<?php echo esc_attr($options['archive_label_format']); ?>"
2537 placeholder="{name}"
2538 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;">
2539 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
2540 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>
2541 </p>
2542 </div>
2543
2544 </div>
2545 <?php
2546 }
2547 }
2548