get_adapters(), fn( $adapter ) => $adapter->is_active() ) ); } /** * Whether any known source SEO plugin is currently active. * * @return bool */ public function has_active_source_plugin(): bool { return ! empty( $this->get_active_adapters() ); } /** * Dry-run preview: how many pages/fields would be affected, without writing anything. * * @param ABDS_SEO_Migration_Adapter $adapter Adapter to preview. * @return array{adapter: string, pages_found: int} */ public function preview( ABDS_SEO_Migration_Adapter $adapter ): array { return array( 'adapter' => $adapter->get_name(), 'pages_found' => $adapter->count_migratable_pages(), ); } /** * Migrates all public post types' SEO data from the given adapter into * ABDS SEO's own postmeta, logging progress to wp_abds_seo_migration_log. * * @param ABDS_SEO_Migration_Adapter $adapter Adapter to migrate from. * @return array{log_id: int, pages_total: int, pages_migrated: int} */ public function migrate( ABDS_SEO_Migration_Adapter $adapter ): array { global $wpdb; $log_table = $wpdb->prefix . 'abds_seo_migration_log'; $post_ids = get_posts( array( 'post_type' => 'any', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ) ); $pages_total = count( $post_ids ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $log_table is built only from $wpdb->prefix plus a hardcoded literal, never user input; $wpdb->insert() safely escapes its values via the format array. Runs once per explicit, admin-triggered migration, not a cacheable read path. $wpdb->insert( $log_table, array( 'source_plugin' => $adapter->get_slug(), 'started_at' => current_time( 'mysql' ), 'pages_total' => $pages_total, 'status' => 'running', ), array( '%s', '%s', '%d', '%s' ) ); $log_id = (int) $wpdb->insert_id; $pages_migrated = 0; foreach ( $post_ids as $post_id ) { if ( $this->migrate_single_post( $adapter, (int) $post_id ) ) { ++$pages_migrated; } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $log_table is built only from $wpdb->prefix plus a hardcoded literal, never user input; $wpdb->update() safely escapes its values via the format array. Runs once per explicit, admin-triggered migration, not a cacheable read path. $wpdb->update( $log_table, array( 'finished_at' => current_time( 'mysql' ), 'pages_migrated' => $pages_migrated, 'status' => 'completed', ), array( 'id' => $log_id ), array( '%s', '%d', '%s' ), array( '%d' ) ); $settings = get_option( self::SETTINGS_OPTION, array() ); $settings['migration_completed'] = true; $settings['migration_source'] = $adapter->get_slug(); update_option( self::SETTINGS_OPTION, $settings ); return array( 'log_id' => $log_id, 'pages_total' => $pages_total, 'pages_migrated' => $pages_migrated, ); } /** * Copies one post's fields from the source adapter into ABDS SEO's * own meta keys. Returns false if the source had nothing to migrate. * * @param ABDS_SEO_Migration_Adapter $adapter Adapter to read from. * @param int $post_id Post ID to migrate. * @return bool True if at least one field was migrated. */ protected function migrate_single_post( ABDS_SEO_Migration_Adapter $adapter, int $post_id ): bool { $title = $adapter->get_meta_title( $post_id ); $description = $adapter->get_meta_description( $post_id ); $canonical = $adapter->get_canonical( $post_id ); $robots = $adapter->get_robots( $post_id ); $og_title = $adapter->get_og_title( $post_id ); $og_desc = $adapter->get_og_description( $post_id ); $focus_kw = $adapter->get_focus_keyword( $post_id ); $has_data = $title || $description || $canonical || $og_title || $og_desc || $focus_kw; if ( ! $has_data ) { return false; } if ( $title ) { update_post_meta( $post_id, self::META_PREFIX . 'title', sanitize_text_field( $title ) ); } if ( $description ) { update_post_meta( $post_id, self::META_PREFIX . 'description', sanitize_text_field( $description ) ); } if ( $canonical ) { update_post_meta( $post_id, self::META_PREFIX . 'canonical', esc_url_raw( $canonical ) ); } update_post_meta( $post_id, self::META_PREFIX . 'noindex', ! empty( $robots['noindex'] ) ? '1' : '' ); update_post_meta( $post_id, self::META_PREFIX . 'nofollow', ! empty( $robots['nofollow'] ) ? '1' : '' ); if ( $og_title ) { update_post_meta( $post_id, self::META_PREFIX . 'og_title', sanitize_text_field( $og_title ) ); } if ( $og_desc ) { update_post_meta( $post_id, self::META_PREFIX . 'og_description', sanitize_text_field( $og_desc ) ); } if ( $focus_kw ) { update_post_meta( $post_id, self::META_PREFIX . 'focus_keyword', sanitize_text_field( $focus_kw ) ); } return true; } /** * Whether a migration has ever completed on this site. * * @return bool */ public function is_migration_completed(): bool { $settings = get_option( self::SETTINGS_OPTION, array() ); return ! empty( $settings['migration_completed'] ); } }