*/ // Abort if this file is accessed directly. if (!defined('ABSPATH')) { exit; } class Metasync_External_Importer { private $db_redirection; private $redirection_importer; public function __construct($db_redirection = null) { $this->db_redirection = $db_redirection; // Initialize redirection importer if DB resource is provided if ($this->db_redirection) { require_once plugin_dir_path(dirname(__FILE__)) . 'redirections/class-metasync-redirection-importer.php'; $this->redirection_importer = new Metasync_Redirection_Importer($this->db_redirection); } } /** * Get available plugins for a specific import type */ public function get_plugins_for_type($type) { $plugins = [ 'yoast' => ['name' => 'Yoast SEO', 'constant' => 'WPSEO_VERSION'], 'rankmath' => ['name' => 'Rank Math', 'constant' => 'RANK_MATH_VERSION'], 'aioseo' => ['name' => 'All in One SEO', 'constant' => 'AIOSEO_VERSION'], 'redirection' => ['name' => 'Redirection', 'constant' => 'REDIRECTION_VERSION'], 'simple301' => ['name' => 'Simple 301 Redirects', 'constant' => 'SIMPLE_301_REDIRECTS_VERSION'], ]; // Filter plugins based on type support $supported_plugins = []; switch ($type) { case 'redirections': if ($this->redirection_importer) { return $this->redirection_importer->get_available_plugins(); } return []; case 'sitemap': case 'robots': // These types are generally supported by the main SEO plugins $supported = ['yoast', 'rankmath', 'aioseo']; foreach ($supported as $slug) { $plugin = $plugins[$slug]; $is_active = defined($plugin['constant']); // Basic data structure similar to redirection importer $supported_plugins[$slug] = [ 'name' => $plugin['name'], 'key' => $slug, 'installed' => $is_active, 'has_data' => $is_active, // Assume data exists if active for now 'count' => 0, // Count not applicable/calculated yet 'version' => $is_active ? constant($plugin['constant']) : '' ]; } break; case 'schema': // Check for actual per-post schema data // Even if plugin is deactivated, we can still import the data $supported = ['yoast', 'rankmath', 'aioseo']; foreach ($supported as $slug) { $plugin = $plugins[$slug]; $is_active = defined($plugin['constant']); $count = 0; // Always check for data, regardless of plugin activation status $has_data = $this->check_schema_data($slug, $count); $supported_plugins[$slug] = [ 'name' => $plugin['name'], 'key' => $slug, 'installed' => $is_active, 'has_data' => $has_data, 'count' => $count, 'version' => $is_active ? constant($plugin['constant']) : '' ]; } break; case 'indexation': // Check for actual per-post SEO data // Even if plugin is deactivated, we can still import the data $supported = ['yoast', 'rankmath', 'aioseo']; foreach ($supported as $slug) { $plugin = $plugins[$slug]; $is_active = defined($plugin['constant']); $count = 0; // Always check for data, regardless of plugin activation status $has_data = $this->check_indexation_data($slug, $count); $supported_plugins[$slug] = [ 'name' => $plugin['name'], 'key' => $slug, 'installed' => $is_active, 'has_data' => $has_data, 'count' => $count, 'version' => $is_active ? constant($plugin['constant']) : '' ]; } break; case 'primary_category': // Check for primary category data from other SEO plugins $supported = ['yoast', 'rankmath', 'aioseo']; foreach ($supported as $slug) { $plugin = $plugins[$slug]; $is_active = defined($plugin['constant']); $count = 0; $has_data = $this->check_primary_category_data($slug, $count); $supported_plugins[$slug] = [ 'name' => $plugin['name'], 'key' => $slug, 'installed' => $is_active, 'has_data' => $has_data, 'count' => $count, 'version' => $is_active ? constant($plugin['constant']) : '' ]; } break; case 'seo_metadata': // Check for actual SEO metadata (titles and descriptions) // Even if plugin is deactivated, we can still import the data $supported = ['yoast', 'rankmath', 'aioseo']; foreach ($supported as $slug) { $plugin = $plugins[$slug]; $is_active = defined($plugin['constant']); $count = 0; // Always check for data, regardless of plugin activation status $has_data = $this->check_seo_metadata($slug, $count); $supported_plugins[$slug] = [ 'name' => $plugin['name'], 'key' => $slug, 'installed' => $is_active, 'has_data' => $has_data, 'count' => $count, 'version' => $is_active ? constant($plugin['constant']) : '' ]; } break; } return $supported_plugins; } /** * Check if indexation data exists for a plugin */ private function check_indexation_data($plugin, &$count) { global $wpdb; $count = 0; switch ($plugin) { case 'yoast': $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE '_yoast_wpseo_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'rankmath': $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE 'rank_math_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'aioseo': $table = $wpdb->prefix . 'aioseo_posts'; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) { $count = (int) $wpdb->get_var(" SELECT COUNT(post_id) FROM {$table} WHERE post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); } break; } return $count > 0; } /** * Check if schema data exists for a plugin */ private function check_schema_data($plugin, &$count) { global $wpdb; $count = 0; switch ($plugin) { case 'yoast': // Check for Yoast schema meta // Yoast Premium stores schema type in _yoast_wpseo_schema_article_type // Free version may use _yoast_wpseo_schema (JSON) $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE (meta_key = '_yoast_wpseo_schema' OR meta_key = '_yoast_wpseo_schema_article_type') AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'rankmath': // Check for Rank Math schema meta (any schema type) // Rank Math uses meta keys like: rank_math_schema_Article, rank_math_schema_BlogPosting, rank_math_schema_Product, etc. // Exclude shortcode schemas (rank_math_shortcode_schema_*) $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE 'rank_math_schema_%' AND meta_key NOT LIKE 'rank_math_shortcode_schema_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'aioseo': // Check for AIOSEO schema in table $table = $wpdb->prefix . 'aioseo_posts'; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) { $count = (int) $wpdb->get_var(" SELECT COUNT(post_id) FROM {$table} WHERE (schema_type IS NOT NULL AND schema_type != '') AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); } break; } return $count > 0; } /** * Check if SEO metadata (titles and descriptions) exists for a plugin */ private function check_seo_metadata($plugin, &$count) { global $wpdb; $count = 0; switch ($plugin) { case 'yoast': // Check for Yoast SEO title or description $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE (meta_key = '_yoast_wpseo_title' OR meta_key = '_yoast_wpseo_metadesc') AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'rankmath': // Check for Rank Math title or description $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE (meta_key = 'rank_math_title' OR meta_key = 'rank_math_description') AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'aioseo': // Check for AIOSEO title or description in their custom table $table = $wpdb->prefix . 'aioseo_posts'; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table) { $count = (int) $wpdb->get_var(" SELECT COUNT(post_id) FROM {$table} WHERE ((title IS NOT NULL AND title != '') OR (description IS NOT NULL AND description != '')) AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); } break; } return $count > 0; } /** * Check if primary category data exists for a plugin */ private function check_primary_category_data($plugin, &$count) { global $wpdb; $count = 0; switch ($plugin) { case 'yoast': $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'rankmath': $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = 'rank_math_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; case 'aioseo': $count = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_aioseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); break; } return $count > 0; } /** * Import primary category from another SEO plugin * * @param string $plugin Plugin slug (yoast, rankmath, aioseo) * @param array $options Import options * @return array Result array with success, imported, skipped, total, etc. */ public function import_primary_category($plugin, $options = []) { $defaults = [ 'overwrite_existing' => false, 'batch_size' => 50, 'offset' => 0, ]; $options = array_merge($defaults, $options); if (!in_array($plugin, ['yoast', 'rankmath', 'aioseo'])) { return [ 'success' => false, 'message' => 'Invalid plugin specified.', ]; } switch ($plugin) { case 'yoast': return $this->import_yoast_primary_category($options); case 'rankmath': return $this->import_rankmath_primary_category($options); case 'aioseo': return $this->import_aioseo_primary_category($options); } return [ 'success' => false, 'message' => 'Unknown error occurred.', ]; } /** * Import primary category from Yoast SEO */ private function import_yoast_primary_category($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; $total_posts = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); $imported_count = 0; $skipped_count = 0; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $term_id = absint(get_post_meta($post_id, '_yoast_wpseo_primary_category', true)); if ($term_id <= 0 || !get_term($term_id, 'category')) { $skipped_count++; continue; } $existing = (int) get_post_meta($post_id, '_metasync_primary_category', true); if ($existing > 0 && !$overwrite) { $skipped_count++; continue; } update_post_meta($post_id, '_metasync_primary_category', $term_id); $imported_count++; } $processed = $offset + count($posts); $has_more = $processed < $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'has_more' => $has_more, ]; } /** * Import primary category from Rank Math */ private function import_rankmath_primary_category($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; $total_posts = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = 'rank_math_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'rank_math_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); $imported_count = 0; $skipped_count = 0; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $term_id = absint(get_post_meta($post_id, 'rank_math_primary_category', true)); if ($term_id <= 0 || !get_term($term_id, 'category')) { $skipped_count++; continue; } $existing = (int) get_post_meta($post_id, '_metasync_primary_category', true); if ($existing > 0 && !$overwrite) { $skipped_count++; continue; } update_post_meta($post_id, '_metasync_primary_category', $term_id); $imported_count++; } $processed = $offset + count($posts); $has_more = $processed < $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'has_more' => $has_more, ]; } /** * Import primary category from AIOSEO */ private function import_aioseo_primary_category($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; $total_posts = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_aioseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_aioseo_primary_category' AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); $imported_count = 0; $skipped_count = 0; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $term_id = absint(get_post_meta($post_id, '_aioseo_primary_category', true)); if ($term_id <= 0 || !get_term($term_id, 'category')) { $skipped_count++; continue; } $existing = (int) get_post_meta($post_id, '_metasync_primary_category', true); if ($existing > 0 && !$overwrite) { $skipped_count++; continue; } update_post_meta($post_id, '_metasync_primary_category', $term_id); $imported_count++; } $processed = $offset + count($posts); $has_more = $processed < $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'has_more' => $has_more, ]; } /** * Import Redirections */ public function import_redirections($plugin) { if (!$this->redirection_importer) { return ['success' => false, 'message' => 'Redirection database not initialized.']; } return $this->redirection_importer->import_from_plugin($plugin); } /** * Import Sitemap Settings */ public function import_sitemap($plugin) { $imported = false; $message = ''; switch ($plugin) { case 'yoast': $options = get_option('wpseo_xml'); if ($options && isset($options['enablexmlsitemap'])) { // Yoast stores sitemap settings in wpseo_xml option // Metasync sitemap is auto-generated, so we just acknowledge the import $message = 'Sitemap settings imported from Yoast.'; $imported = true; } break; case 'rankmath': $options = get_option('rank-math-options-sitemap'); if ($options) { // Import logic here $message = 'Sitemap settings imported from Rank Math.'; $imported = true; } break; case 'aioseo': $options = get_option('aioseo_options'); if ($options && isset($options['sitemap'])) { // Import logic here $message = 'Sitemap settings imported from AIOSEO.'; $imported = true; } break; } if (!$imported) { return ['success' => false, 'message' => 'No sitemap settings found or plugin not active.']; } return ['success' => true, 'message' => $message]; } /** * Import Robots.txt */ public function import_robots($plugin) { $content = ''; switch ($plugin) { case 'yoast': // Yoast doesn't store robots.txt in DB, it edits the file. // But it might have settings for it. // If we are "importing", we might just want to read the current file if managed by them? // Actually, if they have a custom robots.txt editor, they might store it. // Yoast uses the file system directly. $content = $this->get_robots_content_from_file(); break; case 'rankmath': $options = get_option('rank-math-options-general'); if (isset($options['robots_txt_content'])) { $content = $options['robots_txt_content']; } else { $content = $this->get_robots_content_from_file(); } break; case 'aioseo': $options = get_option('aioseo_options'); if (isset($options['tools']['robots']['rules'])) { // AIOSEO stores rules as array, need to reconstruct // For simplicity, let's try reading the file first as it's the source of truth $content = $this->get_robots_content_from_file(); } break; } if (empty($content)) { return ['success' => false, 'message' => 'No robots.txt content found.']; } // Save to Metasync Robots.txt // Load the class if not already loaded if (!class_exists('Metasync_Robots_Txt')) { require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php'; } $robots_class = Metasync_Robots_Txt::get_instance(); $result = $robots_class->write_robots_file($content); if (is_wp_error($result)) { return ['success' => false, 'message' => $result->get_error_message()]; } return ['success' => true, 'message' => 'Robots.txt content imported successfully.']; } private function get_robots_content_from_file() { $robots_file = ABSPATH . 'robots.txt'; if (file_exists($robots_file)) { return file_get_contents($robots_file); } return ''; } /** * Import Indexation Options (Per-Post Robots Meta) */ public function import_indexation($plugin, $options = []) { $defaults = ['batch_size' => 50, 'offset' => 0]; $options = array_merge($defaults, $options); switch ($plugin) { case 'yoast': return $this->import_yoast_indexation($options); case 'rankmath': return $this->import_rankmath_indexation($options); case 'aioseo': return $this->import_aioseo_indexation($options); default: return ['success' => false, 'message' => 'Invalid plugin specified.']; } } /** * Import per-post indexation settings from Yoast SEO */ private function import_yoast_indexation($options = []) { global $wpdb; $imported_count = 0; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); // Get total count (for progress tracking) $total = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE '_yoast_wpseo_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); // Get batch of posts with Yoast robots meta $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key LIKE '_yoast_wpseo_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $has_changes = false; // Get existing Metasync robots meta $metasync_robots = get_post_meta($post_id, 'metasync_common_robots', true); if (!is_array($metasync_robots)) { $metasync_robots = []; } // Import noindex $yoast_noindex = get_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', true); if ($yoast_noindex === '1' && !isset($metasync_robots['noindex'])) { $metasync_robots['noindex'] = 'noindex'; $has_changes = true; } elseif ($yoast_noindex === '2' && !isset($metasync_robots['index'])) { // '2' means 'index' in Yoast $metasync_robots['index'] = 'index'; $has_changes = true; } // Import nofollow $yoast_nofollow = get_post_meta($post_id, '_yoast_wpseo_meta-robots-nofollow', true); if ($yoast_nofollow === '1' && !isset($metasync_robots['nofollow'])) { $metasync_robots['nofollow'] = 'nofollow'; $has_changes = true; } // Import advanced robots (noarchive, nosnippet, noimageindex) $yoast_adv = get_post_meta($post_id, '_yoast_wpseo_meta-robots-adv', true); if (!empty($yoast_adv)) { $adv_directives = explode(',', $yoast_adv); foreach ($adv_directives as $directive) { $directive = trim($directive); if (in_array($directive, ['noarchive', 'nosnippet', 'noimageindex']) && !isset($metasync_robots[$directive])) { $metasync_robots[$directive] = $directive; $has_changes = true; } } } // Also write to _metasync_robots_advanced JSON $robots_advanced = []; if (!empty($yoast_adv)) { $adv_directives = array_map('trim', explode(',', $yoast_adv)); foreach (['noarchive', 'nosnippet', 'noimageindex'] as $dir) { if (in_array($dir, $adv_directives, true)) { $robots_advanced[$dir] = true; } } } // nofollow from Yoast if ($yoast_nofollow === '1') { $robots_advanced['nofollow'] = true; } if (!empty($robots_advanced)) { update_post_meta($post_id, '_metasync_robots_advanced', wp_json_encode($robots_advanced)); } // Import canonical URL $yoast_canonical = get_post_meta($post_id, '_yoast_wpseo_canonical', true); if (!empty($yoast_canonical)) { $existing_canonical = get_post_meta($post_id, 'meta_canonical', true); if (empty($existing_canonical)) { // Validate: never import a corrupted value such as // "http://Array" from third-party storage. $clean_canonical = Metasync_Canonical_Sanitizer::sanitize_for_save($yoast_canonical); if ($clean_canonical !== '') { update_post_meta($post_id, 'meta_canonical', $clean_canonical); $has_changes = true; } } } // Save Metasync robots meta if changes were made if ($has_changes) { if (!empty($metasync_robots)) { update_post_meta($post_id, 'metasync_common_robots', $metasync_robots); } $imported_count++; } // Flush per-post object cache to prevent unbounded memory growth across batches clean_post_cache($post_id); } $processed = $offset + count($posts); $is_complete = $processed >= $total; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => count($posts) - $imported_count, 'total' => $total, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total > 0 ? round(($processed / $total) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts." : "Processing... {$imported_count} imported." ]; } /** * Import per-post indexation settings from Rank Math */ private function import_rankmath_indexation($options = []) { global $wpdb; $imported_count = 0; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); // Get total count (for progress tracking) $total = (int) $wpdb->get_var(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE 'rank_math_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); // Get batch of posts with Rank Math robots meta $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key LIKE 'rank_math_%' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $has_changes = false; // Get existing Metasync robots meta $metasync_robots = get_post_meta($post_id, 'metasync_common_robots', true); if (!is_array($metasync_robots)) { $metasync_robots = []; } // Import robots array $rm_robots = get_post_meta($post_id, 'rank_math_robots', true); if (is_array($rm_robots)) { // Rank Math stores as array like ['noindex', 'nofollow'] foreach ($rm_robots as $directive) { $directive = strtolower(trim($directive)); if (in_array($directive, ['index', 'noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex']) && !isset($metasync_robots[$directive])) { $metasync_robots[$directive] = $directive; $has_changes = true; } } } // Import advanced robots $rm_adv_robots = get_post_meta($post_id, 'rank_math_advanced_robots', true); if (is_array($rm_adv_robots)) { foreach ($rm_adv_robots as $directive) { $directive = strtolower(trim($directive)); if (in_array($directive, ['noarchive', 'nosnippet', 'noimageindex', 'max-snippet', 'max-video-preview', 'max-image-preview']) && !isset($metasync_robots[$directive])) { $metasync_robots[$directive] = $directive; $has_changes = true; } } } // Parse max-* values and write _metasync_robots_advanced JSON $robots_advanced = []; // Boolean directives from rank_math_robots if (is_array($rm_robots)) { foreach (['nofollow', 'noarchive', 'nosnippet', 'noimageindex'] as $dir) { if (in_array($dir, $rm_robots, true)) { $robots_advanced[$dir] = true; } } } // max-* directives from rank_math_advanced_robots if (is_array($rm_adv_robots)) { foreach ($rm_adv_robots as $key => $value) { // Values are formatted like "max-snippet:-1" if (strpos($key, 'max-snippet') !== false && strpos($value, ':') !== false) { $robots_advanced['max_snippet'] = (int) explode(':', $value)[1]; } if (strpos($key, 'max-image-preview') !== false && strpos($value, ':') !== false) { $robots_advanced['max_image_preview'] = explode(':', $value)[1]; } if (strpos($key, 'max-video-preview') !== false && strpos($value, ':') !== false) { $robots_advanced['max_video_preview'] = (int) explode(':', $value)[1]; } } } if (!empty($robots_advanced)) { update_post_meta($post_id, '_metasync_robots_advanced', wp_json_encode($robots_advanced)); } // Import canonical URL $rm_canonical = get_post_meta($post_id, 'rank_math_canonical_url', true); if (!empty($rm_canonical)) { $existing_canonical = get_post_meta($post_id, 'meta_canonical', true); if (empty($existing_canonical)) { // Validate: never import a corrupted value such as // "http://Array" from third-party storage. $clean_canonical = Metasync_Canonical_Sanitizer::sanitize_for_save($rm_canonical); if ($clean_canonical !== '') { update_post_meta($post_id, 'meta_canonical', $clean_canonical); $has_changes = true; } } } // Save Metasync robots meta if changes were made if ($has_changes) { if (!empty($metasync_robots)) { update_post_meta($post_id, 'metasync_common_robots', $metasync_robots); } $imported_count++; } // Flush per-post object cache to prevent unbounded memory growth across batches clean_post_cache($post_id); } $processed = $offset + count($posts); $is_complete = $processed >= $total; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => count($posts) - $imported_count, 'total' => $total, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total > 0 ? round(($processed / $total) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts." : "Processing... {$imported_count} imported." ]; } /** * Import per-post indexation settings from AIOSEO */ private function import_aioseo_indexation($options = []) { global $wpdb; $imported_count = 0; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); // AIOSEO stores data in a custom table $aioseo_table = $wpdb->prefix . 'aioseo_posts'; // Check if table exists $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$aioseo_table'") === $aioseo_table; if (!$table_exists) { return ['success' => false, 'message' => 'AIOSEO table not found.']; } // Get total count (for progress tracking) $total = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$aioseo_table} WHERE post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); // Get batch of posts with AIOSEO settings $posts = $wpdb->get_results($wpdb->prepare(" SELECT post_id, robots_default, robots_noindex, robots_nofollow, robots_noarchive, robots_nosnippet, robots_noimageindex, robots_max_snippet, robots_max_imagepreview, robots_max_videopreview, canonical_url FROM {$aioseo_table} WHERE post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); foreach ($posts as $aioseo_data) { $post_id = $aioseo_data->post_id; $has_changes = false; // Get existing Metasync robots meta $metasync_robots = get_post_meta($post_id, 'metasync_common_robots', true); if (!is_array($metasync_robots)) { $metasync_robots = []; } // Only import if not using default (robots_default = 0) if ($aioseo_data->robots_default == 0) { // Import noindex if ($aioseo_data->robots_noindex == 1 && !isset($metasync_robots['noindex'])) { $metasync_robots['noindex'] = 'noindex'; $has_changes = true; } // Import nofollow if ($aioseo_data->robots_nofollow == 1 && !isset($metasync_robots['nofollow'])) { $metasync_robots['nofollow'] = 'nofollow'; $has_changes = true; } // Import noarchive if ($aioseo_data->robots_noarchive == 1 && !isset($metasync_robots['noarchive'])) { $metasync_robots['noarchive'] = 'noarchive'; $has_changes = true; } // Import nosnippet if ($aioseo_data->robots_nosnippet == 1 && !isset($metasync_robots['nosnippet'])) { $metasync_robots['nosnippet'] = 'nosnippet'; $has_changes = true; } // Import noimageindex if ($aioseo_data->robots_noimageindex == 1 && !isset($metasync_robots['noimageindex'])) { $metasync_robots['noimageindex'] = 'noimageindex'; $has_changes = true; } } // Write _metasync_robots_advanced JSON $robots_advanced = []; if ($aioseo_data->robots_default == 0) { if ($aioseo_data->robots_nofollow == 1) $robots_advanced['nofollow'] = true; if ($aioseo_data->robots_noarchive == 1) $robots_advanced['noarchive'] = true; if ($aioseo_data->robots_nosnippet == 1) $robots_advanced['nosnippet'] = true; if ($aioseo_data->robots_noimageindex == 1) $robots_advanced['noimageindex'] = true; } if (isset($aioseo_data->robots_max_snippet) && is_numeric($aioseo_data->robots_max_snippet)) { $robots_advanced['max_snippet'] = (int) $aioseo_data->robots_max_snippet; } if (!empty($aioseo_data->robots_max_imagepreview)) { $robots_advanced['max_image_preview'] = $aioseo_data->robots_max_imagepreview; } if (isset($aioseo_data->robots_max_videopreview) && is_numeric($aioseo_data->robots_max_videopreview)) { $robots_advanced['max_video_preview'] = (int) $aioseo_data->robots_max_videopreview; } if (!empty($robots_advanced)) { update_post_meta($post_id, '_metasync_robots_advanced', wp_json_encode($robots_advanced)); $has_changes = true; } // Import canonical URL if (!empty($aioseo_data->canonical_url)) { $existing_canonical = get_post_meta($post_id, 'meta_canonical', true); if (empty($existing_canonical)) { // Validate: never import a corrupted value such as // "http://Array" from third-party storage. $clean_canonical = Metasync_Canonical_Sanitizer::sanitize_for_save($aioseo_data->canonical_url); if ($clean_canonical !== '') { update_post_meta($post_id, 'meta_canonical', $clean_canonical); $has_changes = true; } } } // Save Metasync robots meta if changes were made if ($has_changes) { if (!empty($metasync_robots)) { update_post_meta($post_id, 'metasync_common_robots', $metasync_robots); } $imported_count++; } // Flush per-post object cache to prevent unbounded memory growth across batches clean_post_cache($post_id); } $processed = $offset + count($posts); $is_complete = $processed >= $total; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => count($posts) - $imported_count, 'total' => $total, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total > 0 ? round(($processed / $total) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts." : "Processing... {$imported_count} imported." ]; } /** * Import Schema Settings (Per-Post Schema) */ public function import_schema($plugin) { $imported_count = 0; switch ($plugin) { case 'yoast': $imported_count = $this->import_yoast_schema(); break; case 'rankmath': $imported_count = $this->import_rankmath_schema(); break; case 'aioseo': $imported_count = $this->import_aioseo_schema(); break; default: return ['success' => false, 'message' => 'Invalid plugin specified.']; } if ($imported_count > 0) { return ['success' => true, 'message' => "Successfully imported schema settings for $imported_count posts."]; } return ['success' => false, 'message' => 'No post-level schema settings found to import.']; } /** * Import per-post schema from Yoast SEO */ private function import_yoast_schema() { global $wpdb; $imported_count = 0; // First, try to import from full schema JSON (free version or old approach) $posts = $wpdb->get_results(" SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_schema' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; // Check if Metasync schema already exists $existing_schema = get_post_meta($post_id, 'metasync_schema_markup', true); if (!empty($existing_schema) && !empty($existing_schema['types'])) { continue; // Skip if already has Metasync schema } // Decode Yoast schema JSON $yoast_schema = json_decode((string)($post_obj->meta_value ?? ''), true); if (empty($yoast_schema) || !is_array($yoast_schema)) { continue; } // Convert Yoast schema to Metasync format $metasync_schema = $this->convert_yoast_schema_to_metasync($yoast_schema, $post_id); if (!empty($metasync_schema['types'])) { update_post_meta($post_id, 'metasync_schema_markup', $metasync_schema); $imported_count++; } } // Second, try to import from schema type (Premium version) $posts = $wpdb->get_results(" SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_yoast_wpseo_schema_article_type' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; // Check if Metasync schema already exists $existing_schema = get_post_meta($post_id, 'metasync_schema_markup', true); if (!empty($existing_schema) && !empty($existing_schema['types'])) { continue; // Skip if already has Metasync schema } $schema_type = strtolower($post_obj->meta_value); // Create basic article schema with placeholders // Yoast Premium generates schema dynamically, so we create a minimal version if ($schema_type === 'article' || $schema_type === 'newsarticle' || $schema_type === 'blogposting') { $metasync_schema = [ 'enabled' => true, 'types' => [ [ 'type' => 'article', 'fields' => [ 'title_override' => '{{post_title}}', 'description_override' => '{{post_description}}', 'image_override' => '{{featured_image}}', 'organization_name' => '', 'organization_logo' => '' ] ] ] ]; update_post_meta($post_id, 'metasync_schema_markup', $metasync_schema); $imported_count++; } } return $imported_count; } /** * Import per-post schema from Rank Math */ private function import_rankmath_schema() { global $wpdb; $imported_count = 0; // Get all posts with any Rank Math schema (dynamically detect schema types) // Exclude shortcode schemas $posts = $wpdb->get_results(" SELECT DISTINCT pm.post_id, pm.meta_key, pm.meta_value FROM {$wpdb->postmeta} pm WHERE pm.meta_key LIKE 'rank_math_schema_%' AND pm.meta_key NOT LIKE 'rank_math_shortcode_schema_%' AND pm.post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY pm.post_id "); $processed_posts = []; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; // Skip if we already processed this post if (in_array($post_id, $processed_posts)) { continue; } // Check if Metasync schema already exists for this post $existing_schema = get_post_meta($post_id, 'metasync_schema_markup', true); if (!empty($existing_schema) && !empty($existing_schema['types'])) { continue; // Skip if already has Metasync schema } // Extract schema type from meta key (e.g., rank_math_schema_BlogPosting -> BlogPosting) $schema_type = str_replace('rank_math_schema_', '', $post_obj->meta_key); // Decode Rank Math schema $rm_schema = maybe_unserialize($post_obj->meta_value); if (empty($rm_schema) || !is_array($rm_schema)) { continue; } // Convert Rank Math schema to Metasync format $metasync_schema = $this->convert_rankmath_schema_to_metasync($rm_schema, $schema_type, $post_id); if (!empty($metasync_schema['types'])) { update_post_meta($post_id, 'metasync_schema_markup', $metasync_schema); $imported_count++; $processed_posts[] = $post_id; // Mark post as processed } } return $imported_count; } /** * Import per-post schema from AIOSEO */ private function import_aioseo_schema() { global $wpdb; $imported_count = 0; // Check if AIOSEO table exists $table = $wpdb->prefix . 'aioseo_posts'; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) { return 0; } // Get all posts with AIOSEO schema $posts = $wpdb->get_results(" SELECT post_id, schema_type, schema_type_options FROM {$table} WHERE (schema_type IS NOT NULL AND schema_type != '') AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); foreach ($posts as $aioseo_data) { $post_id = $aioseo_data->post_id; // Check if Metasync schema already exists $existing_schema = get_post_meta($post_id, 'metasync_schema_markup', true); if (!empty($existing_schema) && !empty($existing_schema['types'])) { continue; // Skip if already has Metasync schema } // Decode AIOSEO schema options $schema_options = json_decode((string)($aioseo_data->schema_type_options ?? ''), true); if (!is_array($schema_options)) { $schema_options = []; } // Convert AIOSEO schema to Metasync format $metasync_schema = $this->convert_aioseo_schema_to_metasync( $aioseo_data->schema_type, $schema_options, $post_id ); if (!empty($metasync_schema['types'])) { update_post_meta($post_id, 'metasync_schema_markup', $metasync_schema); $imported_count++; } } return $imported_count; } /** * Convert Yoast schema to Metasync format */ private function convert_yoast_schema_to_metasync($yoast_schema, $post_id) { $metasync_schema = [ 'enabled' => true, 'types' => [] ]; // Yoast stores schema as a graph array if (isset($yoast_schema['@graph']) && is_array($yoast_schema['@graph'])) { foreach ($yoast_schema['@graph'] as $item) { if (!isset($item['@type'])) { continue; } $raw_type = $item['@type']; if ( is_array( $raw_type ) ) { $raw_type = reset( $raw_type ); } if ( ! is_string( $raw_type ) || '' === $raw_type ) { continue; } $type = strtolower( $raw_type ); // Map Yoast types to Metasync types if ($type === 'article' || $type === 'newsarticle' || $type === 'blogposting') { $metasync_schema['types'][] = [ 'type' => 'article', 'fields' => [ 'title_override' => isset($item['headline']) ? $item['headline'] : '{{post_title}}', 'description_override' => isset($item['description']) ? $item['description'] : '{{post_description}}', 'image_override' => isset($item['image']) ? (is_array($item['image']) ? $item['image'][0] : $item['image']) : '{{featured_image}}', 'organization_name' => isset($item['publisher']['name']) ? $item['publisher']['name'] : '', 'organization_logo' => isset($item['publisher']['logo']['url']) ? $item['publisher']['logo']['url'] : '' ] ]; } elseif ($type === 'faqpage') { $faq_items = []; if (isset($item['mainEntity']) && is_array($item['mainEntity'])) { foreach ($item['mainEntity'] as $question) { if (isset($question['name']) && isset($question['acceptedAnswer']['text'])) { $faq_items[] = [ 'question' => $question['name'], 'answer' => $question['acceptedAnswer']['text'] ]; } } } if (!empty($faq_items)) { $metasync_schema['types'][] = [ 'type' => 'FAQPage', 'fields' => [ 'faq_items' => $faq_items ] ]; } } elseif ($type === 'product') { $metasync_schema['types'][] = [ 'type' => 'product', 'fields' => [ 'title_override' => isset($item['name']) ? $item['name'] : '{{post_title}}', 'description_override' => isset($item['description']) ? $item['description'] : '{{post_description}}', 'image_override' => isset($item['image']) ? (is_array($item['image']) ? $item['image'][0] : $item['image']) : '{{featured_image}}', 'sku' => isset($item['sku']) ? $item['sku'] : '', 'brand' => isset($item['brand']['name']) ? $item['brand']['name'] : '', 'price' => isset($item['offers']['price']) ? floatval($item['offers']['price']) : 0, 'currency' => isset($item['offers']['priceCurrency']) ? $item['offers']['priceCurrency'] : 'USD', 'availability' => isset($item['offers']['availability']) ? basename($item['offers']['availability']) : 'InStock', 'condition' => isset($item['offers']['itemCondition']) ? basename($item['offers']['itemCondition']) : 'NewCondition' ] ]; } elseif ($type === 'recipe') { $ingredients = []; if (isset($item['recipeIngredient']) && is_array($item['recipeIngredient'])) { $ingredients = $item['recipeIngredient']; } $instructions = []; if (isset($item['recipeInstructions']) && is_array($item['recipeInstructions'])) { foreach ($item['recipeInstructions'] as $step) { if (is_string($step)) { $instructions[] = $step; } elseif (isset($step['text'])) { $instructions[] = $step['text']; } } } $metasync_schema['types'][] = [ 'type' => 'recipe', 'fields' => [ 'title_override' => isset($item['name']) ? $item['name'] : '{{post_title}}', 'description_override' => isset($item['description']) ? $item['description'] : '{{post_description}}', 'image_override' => isset($item['image']) ? (is_array($item['image']) ? $item['image'][0] : $item['image']) : '{{featured_image}}', 'yield' => isset($item['recipeYield']) ? $item['recipeYield'] : '', 'ingredients' => $ingredients, 'instructions' => $instructions, 'prep_time' => isset($item['prepTime']) ? $this->parse_duration($item['prepTime']) : 0, 'cook_time' => isset($item['cookTime']) ? $this->parse_duration($item['cookTime']) : 0, 'total_time' => isset($item['totalTime']) ? $this->parse_duration($item['totalTime']) : 0, 'calories' => isset($item['nutrition']['calories']) ? intval($item['nutrition']['calories']) : 0 ] ]; } } } return $metasync_schema; } /** * Convert Rank Math schema to Metasync format */ private function convert_rankmath_schema_to_metasync($rm_schema, $schema_type, $post_id) { $metasync_schema = [ 'enabled' => true, 'types' => [] ]; $type = strtolower($schema_type); // Handle article-like schema types (Article, BlogPosting, NewsArticle, etc.) if ($type === 'article' || $type === 'blogposting' || $type === 'newsarticle') { $metasync_schema['types'][] = [ 'type' => 'article', 'fields' => [ 'title_override' => $this->normalize_text_value($rm_schema['headline'] ?? null, '{{post_title}}'), 'description_override' => $this->normalize_text_value($rm_schema['description'] ?? null, '{{post_description}}'), 'image_override' => $this->normalize_image_value($rm_schema['image'] ?? null), 'organization_name' => isset($rm_schema['publisher']) ? $rm_schema['publisher'] : '', 'organization_logo' => isset($rm_schema['publisher_logo']) ? $rm_schema['publisher_logo'] : '' ] ]; } elseif ($type === 'faqpage') { $faq_items = []; if (isset($rm_schema['questions']) && is_array($rm_schema['questions'])) { foreach ($rm_schema['questions'] as $question) { if (isset($question['name']) && isset($question['text'])) { $faq_items[] = [ 'question' => $question['name'], 'answer' => $question['text'] ]; } } } if (!empty($faq_items)) { $metasync_schema['types'][] = [ 'type' => 'FAQPage', 'fields' => [ 'faq_items' => $faq_items ] ]; } } elseif ($type === 'product') { $metasync_schema['types'][] = [ 'type' => 'product', 'fields' => [ 'title_override' => $this->normalize_text_value($rm_schema['name'] ?? null, '{{post_title}}'), 'description_override' => $this->normalize_text_value($rm_schema['description'] ?? null, '{{post_description}}'), 'image_override' => $this->normalize_image_value($rm_schema['image'] ?? null), 'sku' => isset($rm_schema['sku']) ? $rm_schema['sku'] : '', 'brand' => isset($rm_schema['brand']) ? $rm_schema['brand'] : '', 'price' => isset($rm_schema['price']) ? floatval($rm_schema['price']) : 0, 'currency' => isset($rm_schema['currency']) ? $rm_schema['currency'] : 'USD', 'availability' => isset($rm_schema['inStock']) ? ($rm_schema['inStock'] ? 'InStock' : 'OutOfStock') : 'InStock', 'condition' => 'NewCondition' ] ]; } elseif ($type === 'recipe') { $metasync_schema['types'][] = [ 'type' => 'recipe', 'fields' => [ 'title_override' => $this->normalize_text_value($rm_schema['name'] ?? null, '{{post_title}}'), 'description_override' => $this->normalize_text_value($rm_schema['description'] ?? null, '{{post_description}}'), 'image_override' => $this->normalize_image_value($rm_schema['image'] ?? null), 'yield' => isset($rm_schema['recipeYield']) ? $rm_schema['recipeYield'] : '', 'ingredients' => isset($rm_schema['recipeIngredient']) ? $rm_schema['recipeIngredient'] : [], 'instructions' => isset($rm_schema['recipeInstructions']) ? $rm_schema['recipeInstructions'] : [], 'prep_time' => isset($rm_schema['prepTime']) ? intval($rm_schema['prepTime']) : 0, 'cook_time' => isset($rm_schema['cookTime']) ? intval($rm_schema['cookTime']) : 0, 'total_time' => isset($rm_schema['totalTime']) ? intval($rm_schema['totalTime']) : 0, 'calories' => isset($rm_schema['calories']) ? intval($rm_schema['calories']) : 0 ] ]; } return $metasync_schema; } /** * Convert AIOSEO schema to Metasync format */ private function convert_aioseo_schema_to_metasync($schema_type, $schema_options, $post_id) { $metasync_schema = [ 'enabled' => true, 'types' => [] ]; $type = strtolower($schema_type); if ($type === 'article') { $metasync_schema['types'][] = [ 'type' => 'article', 'fields' => [ 'title_override' => isset($schema_options['headline']) ? $schema_options['headline'] : '{{post_title}}', 'description_override' => isset($schema_options['description']) ? $schema_options['description'] : '{{post_description}}', 'image_override' => isset($schema_options['image']) ? $schema_options['image'] : '{{featured_image}}', 'organization_name' => isset($schema_options['organizationName']) ? $schema_options['organizationName'] : '', 'organization_logo' => isset($schema_options['organizationLogo']) ? $schema_options['organizationLogo'] : '' ] ]; } elseif ($type === 'faqpage') { $faq_items = []; if (isset($schema_options['questions']) && is_array($schema_options['questions'])) { foreach ($schema_options['questions'] as $question) { if (isset($question['question']) && isset($question['answer'])) { $faq_items[] = [ 'question' => $question['question'], 'answer' => $question['answer'] ]; } } } if (!empty($faq_items)) { $metasync_schema['types'][] = [ 'type' => 'FAQPage', 'fields' => [ 'faq_items' => $faq_items ] ]; } } elseif ($type === 'product') { $metasync_schema['types'][] = [ 'type' => 'product', 'fields' => [ 'title_override' => isset($schema_options['name']) ? $schema_options['name'] : '{{post_title}}', 'description_override' => isset($schema_options['description']) ? $schema_options['description'] : '{{post_description}}', 'image_override' => isset($schema_options['image']) ? $schema_options['image'] : '{{featured_image}}', 'sku' => isset($schema_options['sku']) ? $schema_options['sku'] : '', 'brand' => isset($schema_options['brand']) ? $schema_options['brand'] : '', 'price' => isset($schema_options['price']) ? floatval($schema_options['price']) : 0, 'currency' => isset($schema_options['currency']) ? $schema_options['currency'] : 'USD', 'availability' => isset($schema_options['availability']) ? $schema_options['availability'] : 'InStock', 'condition' => isset($schema_options['condition']) ? $schema_options['condition'] : 'NewCondition' ] ]; } elseif ($type === 'recipe') { $metasync_schema['types'][] = [ 'type' => 'recipe', 'fields' => [ 'title_override' => isset($schema_options['name']) ? $schema_options['name'] : '{{post_title}}', 'description_override' => isset($schema_options['description']) ? $schema_options['description'] : '{{post_description}}', 'image_override' => isset($schema_options['image']) ? $schema_options['image'] : '{{featured_image}}', 'yield' => isset($schema_options['recipeYield']) ? $schema_options['recipeYield'] : '', 'ingredients' => isset($schema_options['recipeIngredient']) ? $schema_options['recipeIngredient'] : [], 'instructions' => isset($schema_options['recipeInstructions']) ? $schema_options['recipeInstructions'] : [], 'prep_time' => isset($schema_options['prepTime']) ? intval($schema_options['prepTime']) : 0, 'cook_time' => isset($schema_options['cookTime']) ? intval($schema_options['cookTime']) : 0, 'total_time' => isset($schema_options['totalTime']) ? intval($schema_options['totalTime']) : 0, 'calories' => isset($schema_options['calories']) ? intval($schema_options['calories']) : 0 ] ]; } return $metasync_schema; } /** * Parse ISO 8601 duration to minutes * e.g., "PT15M" = 15 minutes, "PT1H30M" = 90 minutes */ private function parse_duration($duration) { if (empty($duration)) { return 0; } // Simple parser for PT format $minutes = 0; if (preg_match('/PT(\d+)H/', $duration, $hours)) { $minutes += intval($hours[1]) * 60; } if (preg_match('/(\d+)M/', $duration, $mins)) { $minutes += intval($mins[1]); } return $minutes; } /** * Normalize image value to string URL * Handles arrays from Rank Math/Yoast and converts placeholders */ private function normalize_image_value($image) { if (empty($image)) { return '{{featured_image}}'; } // If it's an array (from Rank Math/Yoast), extract the URL if (is_array($image)) { // Check for 'url' key first if (isset($image['url'])) { $image = $image['url']; } // Check for '@id' key (Yoast format) elseif (isset($image['@id'])) { $image = $image['@id']; } // If it's still an array, try to get first element elseif (isset($image[0])) { $image = is_string($image[0]) ? $image[0] : '{{featured_image}}'; } else { $image = '{{featured_image}}'; } } // Convert common placeholder formats to Metasync format $placeholder_map = [ '%post_thumbnail%' => '{{featured_image}}', '%featured_image%' => '{{featured_image}}', '%seo_title%' => '{{post_title}}', '%post_title%' => '{{post_title}}', '%seo_description%' => '{{post_description}}', '%post_excerpt%' => '{{post_description}}' ]; foreach ($placeholder_map as $old => $new) { if ($image === $old || strpos($image, $old) !== false) { $image = str_replace($old, $new, $image); } } return is_string($image) ? $image : '{{featured_image}}'; } /** * Normalize text value to string * Converts placeholders to Metasync format */ private function normalize_text_value($text, $default = '') { if (empty($text)) { return $default; } // Convert common placeholder formats to Metasync format $placeholder_map = [ '%seo_title%' => '{{post_title}}', '%post_title%' => '{{post_title}}', '%seo_description%' => '{{post_description}}', '%post_excerpt%' => '{{post_description}}' ]; foreach ($placeholder_map as $old => $new) { if (is_string($text) && (strpos($text, $old) !== false || $text === $old)) { $text = str_replace($old, $new, $text); } } return is_string($text) ? $text : $default; } /** * Resolve SEO placeholder tokens to their actual values. * * Tries each source plugin's own replacement engine first so the result * matches exactly what Yoast / Rank Math / AIOSEO would render. Falls back * to manual replacement of the most common tokens. * * @param string $text Raw string that may contain placeholder tokens. * @param int $post_id Post whose context is used for replacement. * @param string $plugin 'yoast', 'rankmath', or 'aioseo'. * @return string Resolved string. */ private function resolve_seo_placeholders($text, $post_id, $plugin) { if (empty($text) || !is_string($text)) { return (string) $text; } $post = get_post($post_id); if (!$post) { return $text; } // Yoast SEO — %%var%% tokens if ($plugin === 'yoast' && class_exists('WPSEO_Replace_Vars')) { try { $replacer = new WPSEO_Replace_Vars(); $resolved = $replacer->replace($text, $post); if (is_string($resolved) && $resolved !== '') { return $resolved; } } catch (Exception $e) { // fall through to manual replacement } } // Rank Math — %var% tokens if ($plugin === 'rankmath' && function_exists('rank_math_replace_vars')) { try { $resolved = rank_math_replace_vars($text, $post); if (is_string($resolved) && $resolved !== '') { return $resolved; } } catch (Exception $e) { // fall through to manual replacement } } // All in One SEO — #var# tokens if ($plugin === 'aioseo') { try { if (function_exists('aioseo') && isset(aioseo()->tags) && method_exists(aioseo()->tags, 'replaceTags')) { $resolved = aioseo()->tags->replaceTags($text, $post_id); if (is_string($resolved) && $resolved !== '') { return $resolved; } } } catch (Exception $e) { // fall through to manual replacement } } return $this->manually_replace_seo_placeholders($text, $post_id); } /** * Manual fallback: replace the most common SEO placeholder tokens from * Yoast (%%var%%), Rank Math (%var%), and AIOSEO (#var#) formats. * * @param string $text Text that may contain placeholder tokens. * @param int $post_id Post ID used for context. * @return string Text with tokens replaced. */ private function manually_replace_seo_placeholders($text, $post_id) { $post = get_post($post_id); if (!$post) { return $text; } $site_name = get_bloginfo('name'); $post_title = get_the_title($post_id); $post_excerpt = has_excerpt($post_id) ? wp_strip_all_tags(get_the_excerpt($post)) : ''; $author = get_the_author_meta('display_name', $post->post_author); $date = get_the_date('', $post_id); $modified = get_the_modified_date('', $post_id); $categories = get_the_category($post_id); $primary_category = !empty($categories) ? $categories[0]->name : ''; $tags = get_the_tags($post_id); $first_tag = !empty($tags) ? $tags[0]->name : ''; // Try to read the separator from whichever plugin is active $sep = '-'; if (defined('WPSEO_VERSION') && class_exists('WPSEO_Options')) { try { $raw = WPSEO_Options::get('separator', 'sc-dash'); // Yoast stores separator keys like 'sc-dash'; convert to glyph // via the public get_separator_options() lookup table. if (class_exists('WPSEO_Option_Titles')) { $options = WPSEO_Option_Titles::get_instance()->get_separator_options(); if (isset($options[$raw])) { $sep = html_entity_decode($options[$raw], ENT_QUOTES, 'UTF-8'); } } } catch (Exception $e) { $sep = '-'; } } elseif (defined('RANK_MATH_VERSION')) { $rm_settings = get_option('rank_math_general_settings', []); if (!empty($rm_settings['title_separator'])) { $sep = html_entity_decode($rm_settings['title_separator'], ENT_QUOTES, 'UTF-8'); } } $map = [ // ── Yoast (%%var%%) ────────────────────────────────────────────── '%%title%%' => $post_title, '%%sitename%%' => $site_name, '%%sep%%' => $sep, '%%excerpt%%' => $post_excerpt, '%%excerpt_only%%' => $post_excerpt, '%%author%%' => $author, '%%date%%' => $date, '%%modified%%' => $modified, '%%id%%' => (string) $post_id, '%%page%%' => '', '%%pagenumber%%' => '', '%%pagetotal%%' => '', '%%primary_category%%' => $primary_category, '%%category%%' => $primary_category, '%%tag%%' => $first_tag, '%%focuskw%%' => (string) get_post_meta($post_id, '_yoast_wpseo_focuskw', true), // ── Rank Math (%var%) ──────────────────────────────────────────── '%title%' => $post_title, '%sitename%' => $site_name, '%sep%' => $sep, '%excerpt%' => $post_excerpt, '%author%' => $author, '%date%' => $date, '%modified%' => $modified, '%id%' => (string) $post_id, '%page%' => '', '%category%' => $primary_category, '%tag%' => $first_tag, '%focus_keyword%' => (string) get_post_meta($post_id, 'rank_math_focus_keyword', true), // ── AIOSEO (#var#) ─────────────────────────────────────────────── '#site_title#' => $site_name, '#post_title#' => $post_title, '#post_excerpt#' => $post_excerpt, '#separator_sa#' => $sep, '#author_name#' => $author, '#current_date#' => $date, '#post_date#' => $date, '#category_title#' => $primary_category, '#tag_title#' => $first_tag, '#id#' => (string) $post_id, ]; // Sort longest token first to avoid partial matches // e.g. %%primary_category%% must replace before %%category%% uksort($map, function ($a, $b) { return strlen($b) - strlen($a); }); return str_replace(array_keys($map), array_values($map), $text); } /** * Import SEO Metadata (Titles and Descriptions) * Supports batch processing via AJAX * * @param string $plugin Plugin to import from (yoast, rankmath, aioseo) * @param array $options Import options (import_titles, import_descriptions, overwrite_existing, batch_size, offset) * @return array Result with success status, progress info, and statistics */ public function import_seo_metadata($plugin, $options = []) { // Default options $defaults = [ 'import_titles' => true, 'import_descriptions' => true, 'overwrite_existing' => false, 'batch_size' => 50, // Process 50 posts per batch 'offset' => 0 ]; $options = array_merge($defaults, $options); // Validate plugin if (!in_array($plugin, ['yoast', 'rankmath', 'aioseo'])) { return [ 'success' => false, 'message' => 'Invalid plugin specified.' ]; } // Route to appropriate import method switch ($plugin) { case 'yoast': return $this->import_yoast_seo_metadata($options); case 'rankmath': return $this->import_rankmath_seo_metadata($options); case 'aioseo': return $this->import_aioseo_seo_metadata($options); } return [ 'success' => false, 'message' => 'Unknown error occurred.' ]; } /** * Import SEO metadata from Yoast SEO */ private function import_yoast_seo_metadata($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $import_titles = (bool) $options['import_titles']; $import_descriptions = (bool) $options['import_descriptions']; $import_social_text = !empty($options['import_social_text']); $import_social_images = !empty($options['import_social_images']); $overwrite = (bool) $options['overwrite_existing']; // Build WHERE clause for meta keys $meta_keys = []; if ($import_titles) { $meta_keys[] = '_yoast_wpseo_title'; } if ($import_descriptions) { $meta_keys[] = '_yoast_wpseo_metadesc'; } if ($import_social_text) { $meta_keys[] = '_yoast_wpseo_opengraph-title'; $meta_keys[] = '_yoast_wpseo_opengraph-description'; $meta_keys[] = '_yoast_wpseo_twitter-title'; $meta_keys[] = '_yoast_wpseo_twitter-description'; } if ($import_social_images) { $meta_keys[] = '_yoast_wpseo_opengraph-image'; $meta_keys[] = '_yoast_wpseo_twitter-image'; } if (!$import_titles && !$import_descriptions && !$import_social_text && !$import_social_images) { return [ 'success' => false, 'message' => 'No import options selected.' ]; } // Get total count (for progress tracking) $meta_keys_placeholders = implode(',', array_fill(0, count($meta_keys), '%s')); $total_posts = (int) $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key IN ($meta_keys_placeholders) AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ", $meta_keys)); // Get batch of posts with Yoast data $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ($meta_keys_placeholders) AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", array_merge($meta_keys, [$batch_size, $offset]))); $imported_count = 0; $skipped_count = 0; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $updated = false; // Import title if ($import_titles) { $yoast_title = get_post_meta($post_id, '_yoast_wpseo_title', true); if (!empty($yoast_title)) { $existing_title = get_post_meta($post_id, '_metasync_seo_title', true); if (empty($existing_title) || $overwrite) { $yoast_title = $this->resolve_seo_placeholders($yoast_title, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_seo_title', sanitize_text_field($yoast_title)); $updated = true; } } } // Import description if ($import_descriptions) { $yoast_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); if (!empty($yoast_desc)) { $existing_desc = get_post_meta($post_id, '_metasync_seo_desc', true); if (empty($existing_desc) || $overwrite) { $yoast_desc = $this->resolve_seo_placeholders($yoast_desc, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_seo_desc', sanitize_textarea_field($yoast_desc)); $updated = true; } } } // Import social text if ($import_social_text) { $yoast_og_title = get_post_meta($post_id, '_yoast_wpseo_opengraph-title', true); if (!empty($yoast_og_title)) { $existing = get_post_meta($post_id, '_metasync_og_title', true); if (empty($existing) || $overwrite) { $yoast_og_title = $this->resolve_seo_placeholders($yoast_og_title, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_og_title', sanitize_text_field($yoast_og_title)); $updated = true; } } $yoast_og_desc = get_post_meta($post_id, '_yoast_wpseo_opengraph-description', true); if (!empty($yoast_og_desc)) { $existing = get_post_meta($post_id, '_metasync_og_description', true); if (empty($existing) || $overwrite) { $yoast_og_desc = $this->resolve_seo_placeholders($yoast_og_desc, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_og_description', sanitize_textarea_field($yoast_og_desc)); $updated = true; } } $yoast_tw_title = get_post_meta($post_id, '_yoast_wpseo_twitter-title', true); if (!empty($yoast_tw_title)) { $existing = get_post_meta($post_id, '_metasync_twitter_title', true); if (empty($existing) || $overwrite) { $yoast_tw_title = $this->resolve_seo_placeholders($yoast_tw_title, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_twitter_title', sanitize_text_field($yoast_tw_title)); $updated = true; } } $yoast_tw_desc = get_post_meta($post_id, '_yoast_wpseo_twitter-description', true); if (!empty($yoast_tw_desc)) { $existing = get_post_meta($post_id, '_metasync_twitter_description', true); if (empty($existing) || $overwrite) { $yoast_tw_desc = $this->resolve_seo_placeholders($yoast_tw_desc, $post_id, 'yoast'); update_post_meta($post_id, '_metasync_twitter_description', sanitize_textarea_field($yoast_tw_desc)); $updated = true; } } } // Import social images if ($import_social_images) { $yoast_og_image = get_post_meta($post_id, '_yoast_wpseo_opengraph-image', true); if (!empty($yoast_og_image)) { $existing_og = get_post_meta($post_id, '_metasync_og_image', true); if (empty($existing_og) || $overwrite) { update_post_meta($post_id, '_metasync_og_image', esc_url_raw($yoast_og_image)); $updated = true; } } $yoast_twitter_image = get_post_meta($post_id, '_yoast_wpseo_twitter-image', true); if (!empty($yoast_twitter_image)) { $existing_twitter = get_post_meta($post_id, '_metasync_twitter_image', true); if (empty($existing_twitter) || $overwrite) { update_post_meta($post_id, '_metasync_twitter_image', esc_url_raw($yoast_twitter_image)); $updated = true; } } } if ($updated) { $imported_count++; } else { $skipped_count++; } } $processed = $offset + count($posts); $is_complete = $processed >= $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total_posts > 0 ? round(($processed / $total_posts) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts, skipped {$skipped_count} posts." : "Processing... {$imported_count} imported, {$skipped_count} skipped." ]; } /** * Import SEO metadata from Rank Math */ private function import_rankmath_seo_metadata($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $import_titles = (bool) $options['import_titles']; $import_descriptions = (bool) $options['import_descriptions']; $import_social_text = !empty($options['import_social_text']); $import_social_images = !empty($options['import_social_images']); $overwrite = (bool) $options['overwrite_existing']; // Build WHERE clause for meta keys $meta_keys = []; if ($import_titles) { $meta_keys[] = 'rank_math_title'; } if ($import_descriptions) { $meta_keys[] = 'rank_math_description'; } if ($import_social_text) { $meta_keys[] = 'rank_math_facebook_title'; $meta_keys[] = 'rank_math_facebook_description'; $meta_keys[] = 'rank_math_twitter_title'; $meta_keys[] = 'rank_math_twitter_description'; } if ($import_social_images) { $meta_keys[] = 'rank_math_facebook_image'; $meta_keys[] = 'rank_math_twitter_image'; } if (!$import_titles && !$import_descriptions && !$import_social_text && !$import_social_images) { return [ 'success' => false, 'message' => 'No import options selected.' ]; } // Get total count $meta_keys_placeholders = implode(',', array_fill(0, count($meta_keys), '%s')); $total_posts = (int) $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key IN ($meta_keys_placeholders) AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ", $meta_keys)); // Get batch of posts $posts = $wpdb->get_results($wpdb->prepare(" SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ($meta_keys_placeholders) AND meta_value != '' AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", array_merge($meta_keys, [$batch_size, $offset]))); $imported_count = 0; $skipped_count = 0; foreach ($posts as $post_obj) { $post_id = $post_obj->post_id; $updated = false; // Import title if ($import_titles) { $rm_title = get_post_meta($post_id, 'rank_math_title', true); if (!empty($rm_title)) { $existing_title = get_post_meta($post_id, '_metasync_seo_title', true); if (empty($existing_title) || $overwrite) { $rm_title = $this->resolve_seo_placeholders($rm_title, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_seo_title', sanitize_text_field($rm_title)); $updated = true; } } } // Import description if ($import_descriptions) { $rm_desc = get_post_meta($post_id, 'rank_math_description', true); if (!empty($rm_desc)) { $existing_desc = get_post_meta($post_id, '_metasync_seo_desc', true); if (empty($existing_desc) || $overwrite) { $rm_desc = $this->resolve_seo_placeholders($rm_desc, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_seo_desc', sanitize_textarea_field($rm_desc)); $updated = true; } } } // Import social text if ($import_social_text) { $rm_og_title = get_post_meta($post_id, 'rank_math_facebook_title', true); if (!empty($rm_og_title)) { $existing = get_post_meta($post_id, '_metasync_og_title', true); if (empty($existing) || $overwrite) { $rm_og_title = $this->resolve_seo_placeholders($rm_og_title, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_og_title', sanitize_text_field($rm_og_title)); $updated = true; } } $rm_og_desc = get_post_meta($post_id, 'rank_math_facebook_description', true); if (!empty($rm_og_desc)) { $existing = get_post_meta($post_id, '_metasync_og_description', true); if (empty($existing) || $overwrite) { $rm_og_desc = $this->resolve_seo_placeholders($rm_og_desc, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_og_description', sanitize_textarea_field($rm_og_desc)); $updated = true; } } $rm_tw_title = get_post_meta($post_id, 'rank_math_twitter_title', true); if (!empty($rm_tw_title)) { $existing = get_post_meta($post_id, '_metasync_twitter_title', true); if (empty($existing) || $overwrite) { $rm_tw_title = $this->resolve_seo_placeholders($rm_tw_title, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_twitter_title', sanitize_text_field($rm_tw_title)); $updated = true; } } $rm_tw_desc = get_post_meta($post_id, 'rank_math_twitter_description', true); if (!empty($rm_tw_desc)) { $existing = get_post_meta($post_id, '_metasync_twitter_description', true); if (empty($existing) || $overwrite) { $rm_tw_desc = $this->resolve_seo_placeholders($rm_tw_desc, $post_id, 'rankmath'); update_post_meta($post_id, '_metasync_twitter_description', sanitize_textarea_field($rm_tw_desc)); $updated = true; } } } // Import social images if ($import_social_images) { $rm_og_image = get_post_meta($post_id, 'rank_math_facebook_image', true); if (!empty($rm_og_image)) { $existing_og = get_post_meta($post_id, '_metasync_og_image', true); if (empty($existing_og) || $overwrite) { update_post_meta($post_id, '_metasync_og_image', esc_url_raw($rm_og_image)); $updated = true; } } $rm_twitter_image = get_post_meta($post_id, 'rank_math_twitter_image', true); if (!empty($rm_twitter_image)) { $existing_twitter = get_post_meta($post_id, '_metasync_twitter_image', true); if (empty($existing_twitter) || $overwrite) { update_post_meta($post_id, '_metasync_twitter_image', esc_url_raw($rm_twitter_image)); $updated = true; } } } if ($updated) { $imported_count++; } else { $skipped_count++; } } $processed = $offset + count($posts); $is_complete = $processed >= $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total_posts > 0 ? round(($processed / $total_posts) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts, skipped {$skipped_count} posts." : "Processing... {$imported_count} imported, {$skipped_count} skipped." ]; } /** * Import SEO metadata from All in One SEO */ private function import_aioseo_seo_metadata($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $import_titles = (bool) $options['import_titles']; $import_descriptions = (bool) $options['import_descriptions']; $import_social_text = !empty($options['import_social_text']); $import_social_images = !empty($options['import_social_images']); $overwrite = (bool) $options['overwrite_existing']; // Check if AIOSEO table exists $table = $wpdb->prefix . 'aioseo_posts'; if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) { return [ 'success' => false, 'message' => 'AIOSEO database table not found.' ]; } if (!$import_titles && !$import_descriptions && !$import_social_text && !$import_social_images) { return [ 'success' => false, 'message' => 'No import options selected.' ]; } // Build WHERE clause $where_conditions = []; if ($import_titles) { $where_conditions[] = '(title IS NOT NULL AND title != \'\')'; } if ($import_descriptions) { $where_conditions[] = '(description IS NOT NULL AND description != \'\')'; } if ($import_social_text) { $where_conditions[] = '(og_title IS NOT NULL AND og_title != \'\')'; $where_conditions[] = '(og_description IS NOT NULL AND og_description != \'\')'; $where_conditions[] = '(twitter_title IS NOT NULL AND twitter_title != \'\')'; $where_conditions[] = '(twitter_description IS NOT NULL AND twitter_description != \'\')'; } if ($import_social_images) { $where_conditions[] = '(og_image_custom_url IS NOT NULL AND og_image_custom_url != \'\')'; $where_conditions[] = '(twitter_image_custom_url IS NOT NULL AND twitter_image_custom_url != \'\')'; } $where_clause = implode(' OR ', $where_conditions); // Build SELECT columns $select_columns = ['post_id']; if ($import_titles) { $select_columns[] = 'title'; } if ($import_descriptions) { $select_columns[] = 'description'; } if ($import_social_text) { $select_columns[] = 'og_title'; $select_columns[] = 'og_description'; $select_columns[] = 'twitter_title'; $select_columns[] = 'twitter_description'; } if ($import_social_images) { $select_columns[] = 'og_image_custom_url'; $select_columns[] = 'twitter_image_custom_url'; } $select_clause = implode(', ', $select_columns); // Get total count $total_posts = (int) $wpdb->get_var(" SELECT COUNT(post_id) FROM {$table} WHERE ({$where_clause}) AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') "); // Get batch of posts $posts = $wpdb->get_results($wpdb->prepare(" SELECT {$select_clause} FROM {$table} WHERE ({$where_clause}) AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish') ORDER BY post_id ASC LIMIT %d OFFSET %d ", $batch_size, $offset)); $imported_count = 0; $skipped_count = 0; foreach ($posts as $aioseo_data) { $post_id = $aioseo_data->post_id; $updated = false; // Import title if ($import_titles && !empty($aioseo_data->title)) { $existing_title = get_post_meta($post_id, '_metasync_seo_title', true); if (empty($existing_title) || $overwrite) { $aioseo_title = $this->resolve_seo_placeholders($aioseo_data->title, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_seo_title', sanitize_text_field($aioseo_title)); $updated = true; } } // Import description if ($import_descriptions && !empty($aioseo_data->description)) { $existing_desc = get_post_meta($post_id, '_metasync_seo_desc', true); if (empty($existing_desc) || $overwrite) { $aioseo_desc = $this->resolve_seo_placeholders($aioseo_data->description, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_seo_desc', sanitize_textarea_field($aioseo_desc)); $updated = true; } } // Import social text if ($import_social_text) { $aioseo_og_title = isset($aioseo_data->og_title) ? $aioseo_data->og_title : ''; if (!empty($aioseo_og_title)) { $existing = get_post_meta($post_id, '_metasync_og_title', true); if (empty($existing) || $overwrite) { $aioseo_og_title = $this->resolve_seo_placeholders($aioseo_og_title, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_og_title', sanitize_text_field($aioseo_og_title)); $updated = true; } } $aioseo_og_desc = isset($aioseo_data->og_description) ? $aioseo_data->og_description : ''; if (!empty($aioseo_og_desc)) { $existing = get_post_meta($post_id, '_metasync_og_description', true); if (empty($existing) || $overwrite) { $aioseo_og_desc = $this->resolve_seo_placeholders($aioseo_og_desc, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_og_description', sanitize_textarea_field($aioseo_og_desc)); $updated = true; } } $aioseo_tw_title = isset($aioseo_data->twitter_title) ? $aioseo_data->twitter_title : ''; if (!empty($aioseo_tw_title)) { $existing = get_post_meta($post_id, '_metasync_twitter_title', true); if (empty($existing) || $overwrite) { $aioseo_tw_title = $this->resolve_seo_placeholders($aioseo_tw_title, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_twitter_title', sanitize_text_field($aioseo_tw_title)); $updated = true; } } $aioseo_tw_desc = isset($aioseo_data->twitter_description) ? $aioseo_data->twitter_description : ''; if (!empty($aioseo_tw_desc)) { $existing = get_post_meta($post_id, '_metasync_twitter_description', true); if (empty($existing) || $overwrite) { $aioseo_tw_desc = $this->resolve_seo_placeholders($aioseo_tw_desc, $post_id, 'aioseo'); update_post_meta($post_id, '_metasync_twitter_description', sanitize_textarea_field($aioseo_tw_desc)); $updated = true; } } } // Import social images if ($import_social_images) { $aioseo_og_image = isset($aioseo_data->og_image_custom_url) ? $aioseo_data->og_image_custom_url : ''; if (!empty($aioseo_og_image)) { $existing_og = get_post_meta($post_id, '_metasync_og_image', true); if (empty($existing_og) || $overwrite) { update_post_meta($post_id, '_metasync_og_image', esc_url_raw($aioseo_og_image)); $updated = true; } } $aioseo_twitter_image = isset($aioseo_data->twitter_image_custom_url) ? $aioseo_data->twitter_image_custom_url : ''; if (!empty($aioseo_twitter_image)) { $existing_twitter = get_post_meta($post_id, '_metasync_twitter_image', true); if (empty($existing_twitter) || $overwrite) { update_post_meta($post_id, '_metasync_twitter_image', esc_url_raw($aioseo_twitter_image)); $updated = true; } } } if ($updated) { $imported_count++; } else { $skipped_count++; } } $processed = $offset + count($posts); $is_complete = $processed >= $total_posts; return [ 'success' => true, 'imported' => $imported_count, 'skipped' => $skipped_count, 'total' => $total_posts, 'processed' => $processed, 'is_complete' => $is_complete, 'progress_percent' => $total_posts > 0 ? round(($processed / $total_posts) * 100) : 100, 'message' => $is_complete ? "Import complete! Imported {$imported_count} posts, skipped {$skipped_count} posts." : "Processing... {$imported_count} imported, {$skipped_count} skipped." ]; } /** * Import term-level SEO metadata from a third-party SEO plugin into * MetaSync term meta (`_metasync_*`). * * Mirrors import_seo_metadata() but operates on terms (wp_termmeta for * Yoast/Rank Math and the wp_aioseo_terms table for AIOSEO) and walks * every registered public taxonomy so category, post_tag, and custom * taxonomies are all covered. * * @param string $plugin One of 'yoast', 'rankmath', 'aioseo'. * @param array $options Batch options (overwrite_existing, batch_size, offset). * @return array ['success'=>bool,'imported'=>int,'skipped'=>int,'total'=>int,'message'=>string] */ public function import_term_seo_metadata($plugin, $options = []) { $defaults = [ 'overwrite_existing' => false, 'batch_size' => 100, 'offset' => 0, ]; $options = array_merge($defaults, $options); if (!in_array($plugin, ['yoast', 'rankmath', 'aioseo'], true)) { return [ 'success' => false, 'message' => 'Invalid plugin specified.', ]; } switch ($plugin) { case 'yoast': return $this->import_yoast_term_meta($options); case 'rankmath': return $this->import_rankmath_term_meta($options); case 'aioseo': return $this->import_aioseo_term_meta($options); } return [ 'success' => false, 'message' => 'Unknown error occurred.', ]; } /** * Import Yoast term meta into MetaSync term meta. * * @param array $options * @return array */ private function import_yoast_term_meta($options) { $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; // Yoast stores taxonomy term SEO data in the `wpseo_taxonomy_meta` // option (wp_options), NOT in wp_termmeta. We must read from there. if (!class_exists('WPSEO_Taxonomy_Meta')) { return [ 'success' => false, 'message' => 'Yoast SEO is not active or WPSEO_Taxonomy_Meta class not available.', ]; } $taxonomies = array_values(get_taxonomies(['public' => true], 'names')); $terms = get_terms([ 'taxonomy' => $taxonomies, 'hide_empty' => false, 'number' => $batch_size, 'offset' => $offset, ]); if (is_wp_error($terms)) { return [ 'success' => false, 'message' => $terms->get_error_message(), ]; } $field_map = [ 'wpseo_title' => '_metasync_metatitle', 'wpseo_desc' => '_metasync_metadesc', 'wpseo_opengraph-title' => '_metasync_og_title', 'wpseo_opengraph-description' => '_metasync_og_description', 'wpseo_canonical' => '_metasync_canonical_url', ]; $imported = 0; $skipped = 0; foreach ($terms as $term) { $term_updated = false; // Read from Yoast's wpseo_taxonomy_meta option via its API. $yoast_meta = WPSEO_Taxonomy_Meta::get_term_meta($term->term_id, $term->taxonomy); if (!is_array($yoast_meta)) { $yoast_meta = []; } foreach ($field_map as $src_key => $dest_key) { $src_value = isset($yoast_meta[$src_key]) ? $yoast_meta[$src_key] : ''; if ($src_value === '' || $src_value === null) { continue; } $existing = get_term_meta($term->term_id, $dest_key, true); if (!empty($existing) && !$overwrite) { continue; } // Validate canonical: never import a corrupted value. if ($dest_key === '_metasync_canonical_url') { $src_value = Metasync_Canonical_Sanitizer::sanitize_for_save($src_value); if ($src_value === '') { continue; } } update_term_meta($term->term_id, $dest_key, $src_value); $term_updated = true; } $noindex = isset($yoast_meta['wpseo_noindex']) ? $yoast_meta['wpseo_noindex'] : ''; if ($noindex === 'noindex') { $existing = get_term_meta($term->term_id, '_metasync_robots_index', true); if (empty($existing) || $overwrite) { update_term_meta($term->term_id, '_metasync_robots_index', 'noindex'); $term_updated = true; } } if ($term_updated) { $imported++; } else { $skipped++; } } $processed = $offset + count($terms); return [ 'success' => true, 'imported' => $imported, 'skipped' => $skipped, 'total' => $processed, 'message' => "Processed {$processed} terms: {$imported} imported, {$skipped} skipped.", ]; } /** * Import Rank Math term meta into MetaSync term meta. * * @param array $options * @return array */ private function import_rankmath_term_meta($options) { $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; $taxonomies = array_values(get_taxonomies(['public' => true], 'names')); $terms = get_terms([ 'taxonomy' => $taxonomies, 'hide_empty' => false, 'number' => $batch_size, 'offset' => $offset, ]); if (is_wp_error($terms)) { return [ 'success' => false, 'message' => $terms->get_error_message(), ]; } $field_map = [ 'rank_math_title' => '_metasync_metatitle', 'rank_math_description' => '_metasync_metadesc', 'rank_math_facebook_title' => '_metasync_og_title', 'rank_math_facebook_description' => '_metasync_og_description', 'rank_math_canonical_url' => '_metasync_canonical_url', ]; $imported = 0; $skipped = 0; foreach ($terms as $term) { $term_updated = false; foreach ($field_map as $src_key => $dest_key) { $src_value = get_term_meta($term->term_id, $src_key, true); if ($src_value === '' || $src_value === null) { continue; } $existing = get_term_meta($term->term_id, $dest_key, true); if (!empty($existing) && !$overwrite) { continue; } // Validate canonical: never import a corrupted value. if ($dest_key === '_metasync_canonical_url') { $src_value = Metasync_Canonical_Sanitizer::sanitize_for_save($src_value); if ($src_value === '') { continue; } } update_term_meta($term->term_id, $dest_key, $src_value); $term_updated = true; } $robots_raw = get_term_meta($term->term_id, 'rank_math_robots', true); $robots = maybe_unserialize($robots_raw); if (is_array($robots) && in_array('noindex', $robots, true)) { $existing = get_term_meta($term->term_id, '_metasync_robots_index', true); if (empty($existing) || $overwrite) { update_term_meta($term->term_id, '_metasync_robots_index', 'noindex'); $term_updated = true; } } if ($term_updated) { $imported++; } else { $skipped++; } } $processed = $offset + count($terms); return [ 'success' => true, 'imported' => $imported, 'skipped' => $skipped, 'total' => $processed, 'message' => "Processed {$processed} terms: {$imported} imported, {$skipped} skipped.", ]; } /** * Import AIOSEO term meta (from the wp_aioseo_terms custom table) into * MetaSync term meta. * * @param array $options * @return array */ private function import_aioseo_term_meta($options) { global $wpdb; $batch_size = intval($options['batch_size']); $offset = intval($options['offset']); $overwrite = (bool) $options['overwrite_existing']; $table = $wpdb->prefix . 'aioseo_terms'; $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); if ($table_exists !== $table) { return [ 'success' => false, 'message' => 'AIOSEO terms table not found.', ]; } $rows = $wpdb->get_results($wpdb->prepare( "SELECT term_id, title, description, og_title, og_description, canonical_url, robots_noindex FROM {$table} ORDER BY term_id ASC LIMIT %d OFFSET %d", $batch_size, $offset )); $imported = 0; $skipped = 0; foreach ($rows as $row) { $term_id = (int) $row->term_id; if ($term_id <= 0) { continue; } $term_updated = false; $field_map = [ 'title' => '_metasync_metatitle', 'description' => '_metasync_metadesc', 'og_title' => '_metasync_og_title', 'og_description' => '_metasync_og_description', 'canonical_url' => '_metasync_canonical_url', ]; foreach ($field_map as $column => $dest_key) { $value = isset($row->$column) ? $row->$column : ''; if ($value === '' || $value === null) { continue; } $existing = get_term_meta($term_id, $dest_key, true); if (!empty($existing) && !$overwrite) { continue; } // Validate canonical: never import a corrupted value. if ($dest_key === '_metasync_canonical_url') { $value = Metasync_Canonical_Sanitizer::sanitize_for_save($value); if ($value === '') { continue; } } update_term_meta($term_id, $dest_key, $value); $term_updated = true; } if (!empty($row->robots_noindex) && (int) $row->robots_noindex === 1) { $existing = get_term_meta($term_id, '_metasync_robots_index', true); if (empty($existing) || $overwrite) { update_term_meta($term_id, '_metasync_robots_index', 'noindex'); $term_updated = true; } } if ($term_updated) { $imported++; } else { $skipped++; } } $processed = $offset + count($rows); return [ 'success' => true, 'imported' => $imported, 'skipped' => $skipped, 'total' => $processed, 'message' => "Processed {$processed} terms: {$imported} imported, {$skipped} skipped.", ]; } }