PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-activator.php +8 -209 2.7.2 → 2.7.1 View file →
@@ -39,22 +39,8 @@
39 39 */
40 40 const IMPORT_RETRY_BACKOFF_SECONDS = 3600;
41 41
42 42 /**
43 - * The white-label settings whose values are mirrored into the
44 - * metasync.php plugin header.
45 - *
46 - * @var string[]
47 - */
48 - const BRANDING_HEADER_FIELDS = [
49 - 'white_label_plugin_name',
50 - 'white_label_plugin_description',
51 - 'white_label_plugin_author',
52 - 'white_label_plugin_author_uri',
53 - 'white_label_plugin_uri',
54 - ];
55 -
56 - /**
57 43 * Machine-readable reason for the most recent import failure, consumed
58 44 * by render_whitelabel_import_failure_notice().
59 45 *
60 46 * @var string
@@ -61,17 +47,8 @@
61 47 */
62 48 private static $last_import_error = '';
63 49
64 50 /**
65 - * Field-specific validation errors for the most recent import failure,
66 - * keyed by white-label setting name. Populated only when the import was
67 - * rejected because a branding value fails the plugin-header contract.
68 - *
69 - * @var array<string, string>
70 - */
71 - private static $last_import_invalid_fields = array();
72 -
73 - /**
74 51 * Canonical list of MetaSync custom WP-Cron hooks.
75 52 * Shared with Metasync_Deactivator so deactivation cleans up every scheduled hook.
76 53 *
77 54 * @since 2.5.x
@@ -304,17 +281,13 @@
304 281 }
305 282
306 283 // File has changed, import settings
307 284 if (!self::import_whitelabel_settings()) {
308 - $failure = array(
285 + update_option(self::IMPORT_FAILURE_OPTION, array(
309 286 'file_hash' => $file_hash,
310 287 'failed_at' => time(),
311 288 'error' => self::$last_import_error,
312 - );
313 - if (self::$last_import_error === 'branding_field_invalid' && !empty(self::$last_import_invalid_fields)) {
314 - $failure['invalid_fields'] = self::$last_import_invalid_fields;
315 - }
316 - update_option(self::IMPORT_FAILURE_OPTION, $failure);
289 + ));
317 290 return false;
318 291 }
319 292 delete_option(self::IMPORT_FAILURE_OPTION);
320 293
@@ -336,18 +309,8 @@
336 309 return self::$last_import_error;
337 310 }
338 311
339 312 /**
340 - * Field-specific branding errors of the most recent failed White Label import.
341 - *
342 - * @return array<string, string>
343 - */
344 - public static function get_last_import_invalid_fields()
345 - {
346 - return self::$last_import_invalid_fields;
347 - }
348 -
349 - /**
350 313 * Admin notice for a White Label JSON import that keeps failing.
351 314 * Registered from metasync_check_whitelabel_on_admin(); the state is set
352 315 * by check_whitelabel_settings_update() when an import fails.
353 316 *
@@ -366,29 +329,16 @@
366 329 'json_read_failed' => 'the file could not be read',
367 330 'json_invalid' => 'the file is not valid JSON',
368 331 'json_structure_invalid' => 'the file structure is invalid',
369 332 'plugin_file_unreadable' => 'metasync.php could not be read',
370 - 'branding_field_invalid' => 'the white-label branding values are not valid',
371 333 'plugin_header_write_failed' => 'the plugin file headers could not be updated (is metasync.php writable?)',
372 334 'recovery_lock_busy' => 'another password operation was in progress',
373 335 'options_write_failed' => 'the settings could not be saved',
374 336 );
375 337 $reason = isset($failure['error'], $reasons[$failure['error']]) ? $reasons[$failure['error']] : 'an unexpected error occurred';
376 -
377 - // Field-specific detail for a content rejection: name the exact
378 - // branding field so the package can be fixed rather than retried blind.
379 338 echo '<div class="notice notice-error"><p><strong>White Label settings import failed.</strong> '
380 339 . 'MetaSync could not import <code>whitelabel-settings.json</code>: ' . esc_html($reason)
381 - . '. The import will be retried automatically; fix the file and reload to retry immediately.</p>';
382 - if (isset($failure['invalid_fields']) && is_array($failure['invalid_fields']) && !empty($failure['invalid_fields'])) {
383 - echo '<ul style="margin: 8px 0 0 0; list-style: disc; padding-left: 20px;">';
384 - foreach ($failure['invalid_fields'] as $message) {
385 - echo '<li>' . esc_html((string) $message) . '</li>';
386 - }
387 - echo '</ul>';
388 - echo '<p style="margin: 8px 0 0 0;">Allowed: single-line text (including valid Unicode) and http(s) URLs. Line breaks, control/invisible characters, comment-ending text, PHP tags, and non-http(s) URLs are rejected.</p>';
389 - }
390 - echo '</div>';
340 + . '. The import will be retried automatically; fix the file and reload to retry immediately.</p></div>';
391 341 }
392 342
393 343 /**
394 344 * Import whitelabel settings from JSON file if available
@@ -405,9 +355,8 @@
405 355 return false;
406 356 }
407 357
408 358 self::$last_import_error = 'unknown';
409 - self::$last_import_invalid_fields = array();
410 359
411 360 // Read JSON file
412 361 $json_content = file_get_contents($json_file);
413 362 if ($json_content === false) {
@@ -478,16 +427,8 @@
478 427 'general_settings' => $options['general'] ?? array(),
479 428 );
480 429 if (!self::update_plugin_file_headers($header_data, $plugin_file)) {
481 430 self::$last_import_error = 'plugin_header_write_failed';
482 - // Distinguish a content rejection (invalid branding values) from an
483 - // environmental failure (unwritable file, mangled header block) so the
484 - // admin notice can point at the offending field.
485 - $invalid_fields = self::validate_plugin_header_settings($options['general'] ?? array());
486 - if (!empty($invalid_fields)) {
487 - self::$last_import_error = 'branding_field_invalid';
488 - self::$last_import_invalid_fields = $invalid_fields;
489 - }
490 431 return false;
491 432 }
492 433
493 434 // Restore bundled icon: if the icon value is a __bundled_icon__{ext} marker,
@@ -612,134 +553,8 @@
612 553 return self::update_plugin_file_headers($import_data);
613 554 }
614 555
615 556 /**
616 - * Absolute path of the plugin file whose header carries the branding.
617 - *
618 - * @return string
619 - */
620 - public static function plugin_header_file_path()
621 - {
622 - return plugin_dir_path(dirname(__FILE__)) . 'metasync.php';
623 - }
624 -
625 - /**
626 - * Apply a proposed save's branding to the physical plugin header BEFORE
627 - * the options write starts, so an unwritable or malformed target stops
628 - * the save while nothing has been persisted yet.
629 - *
630 - * Returns null when none of the branding values changed (the file is left
631 - * untouched — unrelated saves must not rewrite metasync.php), true when
632 - * the header now matches the proposed values, and false when the update
633 - * was rejected or failed (file unchanged, save must be aborted).
634 - *
635 - * @param mixed $proposed_general General section proposed for saving.
636 - * @param mixed $stored_general General section currently stored.
637 - * @return bool|null
638 - */
639 - public static function apply_plugin_file_headers_for_save($proposed_general, $stored_general)
640 - {
641 - $proposed_general = is_array($proposed_general) ? $proposed_general : array();
642 - $stored_general = is_array($stored_general) ? $stored_general : array();
643 -
644 - $changed = false;
645 - foreach (self::BRANDING_HEADER_FIELDS as $key) {
646 - if ((string) ($proposed_general[$key] ?? '') !== (string) ($stored_general[$key] ?? '')) {
647 - $changed = true;
648 - break;
649 - }
650 - }
651 - if (!$changed) {
652 - return null;
653 - }
654 -
655 - return self::update_plugin_file_headers(array('general_settings' => $proposed_general));
656 - }
657 -
658 - /**
659 - * Restore a previous metasync.php content after a save that wrote new
660 - * headers but then failed to persist the matching options.
661 - *
662 - * @param string $plugin_file Absolute path to metasync.php.
663 - * @param string|false $original_content Content captured before the header write (file_get_contents() result).
664 - * @return bool Whether the restore succeeded.
665 - */
666 - public static function restore_plugin_file_content($plugin_file, $original_content)
667 - {
668 - if (!is_string($original_content) || $original_content === '') {
669 - return false;
670 - }
671 - if (!self::atomically_replace_plugin_file($plugin_file, $original_content)) {
672 - return false;
673 - }
674 - wp_cache_delete('plugins', 'plugins');
675 - return true;
676 - }
677 -
678 - /**
679 - * Validate the five values that are written into metasync.php's plugin header.
680 - *
681 - * This is deliberately shared by administrator saves and package imports so
682 - * both paths enforce the same contract before options are persisted.
683 - *
684 - * @param mixed $general General settings submitted or imported.
685 - * @return array<string, string> Field-specific errors, keyed by setting name.
686 - */
687 - public static function validate_plugin_header_settings($general)
688 - {
689 - $general = is_array($general) ? $general : array();
690 - $header_map = array(
691 - 'white_label_plugin_name' => array(
692 - 'label' => 'Plugin Name',
693 - 'default' => 'Search Atlas SEO - OTTO AI SEO Automation for WordPress',
694 - 'url' => false,
695 - ),
696 - 'white_label_plugin_description' => array(
697 - 'label' => 'Plugin Description',
698 - 'default' => 'Search Atlas SEO is an intuitive WordPress Plugin that transforms the most complicated, most labor-intensive SEO tasks into streamlined, straightforward processes. With a few clicks, the meta-bulk update feature automates the re-optimization of meta tags using AI to increase clicks. Stay up-to-date with the freshest Google Search data for your entire site or targeted URLs within the Meta Sync plug-in page.',
699 - 'url' => false,
700 - ),
701 - 'white_label_plugin_author' => array(
702 - 'label' => 'Author',
703 - 'default' => 'Search Atlas',
704 - 'url' => false,
705 - ),
706 - 'white_label_plugin_author_uri' => array(
707 - 'label' => 'Author URI',
708 - 'default' => 'https://searchatlas.com',
709 - 'url' => true,
710 - ),
711 - 'white_label_plugin_uri' => array(
712 - 'label' => 'Plugin URI',
713 - 'default' => 'https://searchatlas.com/',
714 - 'url' => true,
715 - ),
716 - );
717 - $errors = array();
718 -
719 - foreach ($header_map as $key => $config) {
720 - // A present-but-empty string resets to the default; a scalar like
721 - // "0" is a real value and must not be conflated with unset (the
722 - // classic PHP !empty() trap). Malformed non-strings fall through
723 - // to the is_string rejection below.
724 - $value = $config['default'];
725 - if (array_key_exists($key, $general)) {
726 - $raw = $general[$key];
727 - if (is_string($raw)) {
728 - $value = $raw !== '' ? $raw : $config['default'];
729 - } elseif ($raw !== null) {
730 - $value = $raw;
731 - }
732 - }
733 - if (!is_string($value) || !self::is_valid_plugin_header_value($value, $config['url'])) {
734 - $errors[$key] = sprintf('%s contains invalid characters or format.', $config['label']);
735 - }
736 - }
737 -
738 - return $errors;
739 - }
740 -
741 - /**
742 557 * Update the main plugin file headers with whitelabel values
743 558 * WordPress reads plugin metadata directly from the file header comments,
744 559 * so modifying these ensures whitelabel shows even when the plugin is deactivated.
745 560 *
@@ -792,21 +607,12 @@
792 607
793 608 foreach ($header_map as $setting_key => $field_config) {
794 609 $header_field = $field_config['header'];
795 610
796 - // Use whitelabel value if set, otherwise restore default. A scalar
797 - // like "0" is a real value, not an unset field; an empty string or
798 - // null restores the default; malformed non-strings are rejected by
799 - // the is_string check below.
800 - $new_value = $field_config['default'];
801 - if (array_key_exists($setting_key, $general)) {
802 - $raw_value = $general[$setting_key];
803 - if (is_string($raw_value)) {
804 - $new_value = $raw_value !== '' ? $raw_value : $field_config['default'];
805 - } elseif ($raw_value !== null) {
806 - $new_value = $raw_value;
807 - }
808 - }
611 + // Use whitelabel value if set, otherwise restore default
612 + $new_value = !empty($general[$setting_key])
613 + ? $general[$setting_key]
614 + : $field_config['default'];
809 615
810 616 if (!is_string($new_value) || !self::is_valid_plugin_header_value($new_value, in_array($setting_key, $url_fields, true))) {
811 617 return false;
812 618 }
@@ -857,17 +663,10 @@
857 663 }
858 664
859 665 /**
860 666 * Validate a value before placing it inside the plugin's PHP docblock.
861 - *
862 - * Public so form/import handlers can pre-validate a value with the exact
863 - * contract used when metasync.php is physically rewritten.
864 - *
865 - * @param string $value Candidate header value.
866 - * @param bool $is_url Whether the value must be an http(s) URL.
867 - * @return bool
868 667 */
869 - public static function is_valid_plugin_header_value($value, $is_url)
668 + private static function is_valid_plugin_header_value($value, $is_url)
870 669 {
871 670 if (
872 671 $value === ''
873 672 || preg_match('/^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]+$/u', $value) !== 1