'Yoast SEO', 'has_data' => $this->yoast_has_data(), 'description' => __( 'Imports Yoast Article/WebPage schemas and focus keywords (used for the SERP scan).', 'aiforya-schema-markup' ), ); } if ( class_exists( 'RankMath' ) || defined( 'RANK_MATH_VERSION' ) || is_plugin_active( 'seo-by-rank-math/rank-math.php' ) ) { $detected['rankmath'] = array( 'name' => 'Rank Math SEO', 'has_data' => $this->rankmath_has_data(), 'description' => __( 'Imports Rank Math schemas (Article/Product/Recipe/Event/HowTo/FAQ) with their custom structure.', 'aiforya-schema-markup' ), ); } if ( defined( 'WP_SCHEMA_PRO_VER' ) || is_plugin_active( 'wp-schema-pro/wp-schema-pro.php' ) ) { $detected['schema_pro'] = array( 'name' => 'Schema Pro', 'has_data' => $this->schema_pro_has_data(), 'description' => __( 'Imports Schema Pro global schemas and their ACF/CPT mappings.', 'aiforya-schema-markup' ), ); } if ( defined( 'AIOSEO_VERSION' ) || is_plugin_active( 'all-in-one-seo-pack/all_in_one_seo_pack.php' ) ) { $detected['aioseo'] = array( 'name' => 'All in One SEO', 'has_data' => $this->aioseo_has_data(), 'description' => __( 'Imports AIOSEO schemas (Article/Product/Recipe/Event/Organization).', 'aiforya-schema-markup' ), ); } return $detected; } private function yoast_has_data(): bool { global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key IN ('_yoast_wpseo_focuskw','_yoast_wpseo_schema_article_type','_yoast_wpseo_schema_page_type') LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $count > 0; } private function rankmath_has_data(): bool { global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key LIKE 'rank_math_schema_%' LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $count > 0; } private function schema_pro_has_data(): bool { $opt = get_option( 'wp-schema-pro-global-schemas', array() ); return ! empty( $opt ); } private function aioseo_has_data(): bool { global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key IN ('_aioseo_schema_type','_aioseo_schema_type_options') LIMIT 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $count > 0; } /** * Lance l'import depuis un plugin source. */ public function import_from( string $source ): array { global $wpdb; $log_table = $wpdb->prefix . 'aiforya_sm_migration_log'; $now = current_time( 'mysql' ); $wpdb->insert( $log_table, array( 'source_plugin' => $source, 'status' => 'running', 'started_at' => $now, ) ); $log_id = (int) $wpdb->insert_id; $stats = array( 'ok' => 0, 'fail' => 0, 'details' => array() ); switch ( $source ) { case 'yoast': $stats = $this->import_yoast(); break; case 'rankmath': $stats = $this->import_rankmath(); break; case 'schema_pro': $stats = $this->import_schema_pro(); break; case 'aioseo': $stats = $this->import_aioseo(); break; default: $wpdb->update( $log_table, array( 'status' => 'failed', 'completed_at' => current_time( 'mysql' ), 'details' => wp_json_encode( array( 'error' => 'Unknown source' ) ), ), array( 'id' => $log_id ) ); return array( 'ok' => 0, 'fail' => 0, 'message' => 'That source plugin is not supported.' ); } $wpdb->update( $log_table, array( 'status' => 'completed', 'completed_at' => current_time( 'mysql' ), 'posts_imported' => (int) $stats['ok'], 'posts_failed' => (int) $stats['fail'], 'details' => wp_json_encode( $stats['details'] ?? array() ), ), array( 'id' => $log_id ) ); return array( 'ok' => (int) $stats['ok'], 'fail' => (int) $stats['fail'], 'log_id' => $log_id, ); } /** * Yoast import : convertit les meta _yoast_wpseo_schema_* en schemas AIFORYA. */ private function import_yoast(): array { global $wpdb; $rows = $wpdb->get_results( "SELECT pm.post_id, pm1.meta_value AS article_type, pm2.meta_value AS page_type FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_yoast_wpseo_schema_article_type' LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_yoast_wpseo_schema_page_type' WHERE p.post_status = 'publish' AND p.post_type IN ('post','page','product') AND (pm1.meta_value IS NOT NULL OR pm2.meta_value IS NOT NULL) LIMIT 500", ARRAY_A ); return $this->generate_for_rows( $rows ?: array(), function ( $r ) { $type = $r['article_type'] ?: $r['page_type'] ?: 'WebPage'; // Yoast values like "BlogPosting" / "NewsArticle" / "WebPage" sont déjà Schema.org. return is_string( $type ) ? $type : 'WebPage'; } ); } private function import_rankmath(): array { global $wpdb; // Rank Math stocke les meta sous `rank_math_schema_` (ex: rank_math_schema_Article). $rows = $wpdb->get_results( "SELECT pm.post_id, pm.meta_key FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_status = 'publish' WHERE pm.meta_key LIKE 'rank_math_schema_%' LIMIT 500", ARRAY_A ); return $this->generate_for_rows( $rows ?: array(), function ( $r ) { $key = (string) ( $r['meta_key'] ?? '' ); $type = preg_replace( '/^rank_math_schema_/', '', $key ); return $type ?: 'Article'; } ); } private function import_schema_pro(): array { $opt = get_option( 'wp-schema-pro-global-schemas', array() ); if ( ! is_array( $opt ) || empty( $opt ) ) return array( 'ok' => 0, 'fail' => 0, 'details' => array() ); // Schema Pro : les schémas globaux ont une key 'type' (ex: 'article', 'product', etc.). $stats = array( 'ok' => 0, 'fail' => 0, 'details' => array() ); foreach ( $opt as $sch ) { $type = ucfirst( strtolower( (string) ( $sch['type'] ?? 'Article' ) ) ); // Schema Pro a des règles d'application (post_type, taxonomy) — on applique au post_type principal. $post_type = (string) ( $sch['target']['post-types'][0] ?? 'post' ); $posts = get_posts( array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => 100, 'fields' => 'ids', ) ); foreach ( $posts as $pid ) { $r = $this->apply_type_to_post( (int) $pid, $type ); if ( $r ) $stats['ok']++; else $stats['fail']++; } } return $stats; } private function import_aioseo(): array { global $wpdb; $rows = $wpdb->get_results( "SELECT post_id, meta_value AS aioseo_type FROM {$wpdb->postmeta} WHERE meta_key = '_aioseo_schema_type' LIMIT 500", ARRAY_A ); return $this->generate_for_rows( $rows ?: array(), function ( $r ) { $type = (string) ( $r['aioseo_type'] ?? '' ); // AIOSEO retourne les noms en lowercase (article/product/recipe/event/organization). $map = array( 'article' => 'Article', 'webpage' => 'WebPage', 'product' => 'Product', 'recipe' => 'Recipe', 'event' => 'Event', 'organization' => 'Organization', 'person' => 'Person', 'localbusiness'=> 'LocalBusiness', 'video' => 'VideoObject', ); $key = strtolower( $type ); return $map[ $key ] ?? ucfirst( $type ?: 'Article' ); } ); } /** * Helper commun : itère sur les rows, détermine le type via callback, appelle apply_type_to_post. */ private function generate_for_rows( array $rows, callable $type_resolver ): array { $stats = array( 'ok' => 0, 'fail' => 0, 'details' => array() ); $seen_posts = array(); foreach ( $rows as $r ) { $pid = (int) ( $r['post_id'] ?? 0 ); if ( $pid <= 0 || isset( $seen_posts[ $pid ] ) ) continue; $seen_posts[ $pid ] = true; $type = call_user_func( $type_resolver, $r ); $r2 = $this->apply_type_to_post( $pid, $type ); if ( $r2 ) $stats['ok']++; else $stats['fail']++; } return $stats; } private function apply_type_to_post( int $post_id, string $type ): bool { if ( ! class_exists( 'Aiforya_Schema_Markup_Generator' ) || ! class_exists( 'Aiforya_Schema_Markup_Db' ) ) return false; $valid = Aiforya_Schema_Markup_Detector::supported_schema_types(); if ( ! in_array( $type, $valid, true ) ) $type = 'Article'; $generator = Aiforya_Schema_Markup_Generator::get_instance(); $json = $generator->generate_json_ld( $post_id, $type ); if ( is_wp_error( $json ) ) return false; $r = Aiforya_Schema_Markup_Db::get_instance()->save_or_update_schema( array( 'post_id' => $post_id, 'schema_type' => $type, 'schema_json' => $json, 'is_manual_override' => 1, 'source' => 'migrated', ) ); return ! is_wp_error( $r ); } /** * Liste les imports passés. */ public function get_history( int $limit = 20 ): array { global $wpdb; $table = $wpdb->prefix . 'aiforya_sm_migration_log'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i ORDER BY started_at DESC LIMIT %d", $table, $limit ) ); return is_array( $rows ) ? $rows : array(); } }