wpseo_title/wpseo_desc/...). */ final class YoastImporter extends BaseSeoImporter { private const POST_META_KEYS = array( '_yoast_wpseo_title', '_yoast_wpseo_metadesc', '_yoast_wpseo_canonical', '_yoast_wpseo_meta-robots-noindex', '_yoast_wpseo_meta-robots-nofollow', '_yoast_wpseo_opengraph-image-id', '_yoast_wpseo_opengraph-image', ); public function get_id(): string { return 'yoast'; } public function get_label(): string { return 'Yoast SEO'; } public function is_available(): bool { if ( array() !== $this->source_post_ids() ) { return true; } $taxonomy_meta = get_option( 'wpseo_taxonomy_meta', array() ); return is_array( $taxonomy_meta ) && array() !== $taxonomy_meta; } protected function run( bool $dry, bool $overwrite ): ImportReport { $report = new ImportReport(); foreach ( $this->source_post_ids() as $post_id ) { ++$report->posts; $this->import_post_field( $report, $dry, $overwrite, $post_id, MetaFields::TITLE, $this->convert_variables( (string) get_post_meta( $post_id, '_yoast_wpseo_title', true ) ) ); $this->import_post_field( $report, $dry, $overwrite, $post_id, MetaFields::DESCRIPTION, $this->convert_variables( (string) get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ) ) ); $this->import_post_field( $report, $dry, $overwrite, $post_id, MetaFields::CANONICAL, (string) get_post_meta( $post_id, '_yoast_wpseo_canonical', true ) ); $noindex = (string) get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true ); if ( '1' === $noindex ) { $this->import_post_flag( $report, $dry, $overwrite, $post_id, MetaFields::ROBOTS_INDEX, false ); } elseif ( '2' === $noindex ) { $this->import_post_flag( $report, $dry, $overwrite, $post_id, MetaFields::ROBOTS_INDEX, true ); } if ( '1' === (string) get_post_meta( $post_id, '_yoast_wpseo_meta-robots-nofollow', true ) ) { $this->import_post_flag( $report, $dry, $overwrite, $post_id, MetaFields::ROBOTS_FOLLOW, false ); } // Yoast stores the OG image both as an attachment ID (preferred, // direct mapping) and as a URL (fallback for older data). $og_image_id = (int) get_post_meta( $post_id, '_yoast_wpseo_opengraph-image-id', true ); if ( $og_image_id <= 0 ) { $og_image_id = $this->attachment_id_from_url( (string) get_post_meta( $post_id, '_yoast_wpseo_opengraph-image', true ) ); } $this->import_post_og_image( $report, $dry, $overwrite, $post_id, $og_image_id ); } $this->run_terms( $report, $dry, $overwrite ); return $report; } private function run_terms( ImportReport $report, bool $dry, bool $overwrite ): void { $taxonomy_meta = get_option( 'wpseo_taxonomy_meta', array() ); if ( ! is_array( $taxonomy_meta ) ) { return; } foreach ( $taxonomy_meta as $terms ) { foreach ( (array) $terms as $term_id => $term_data ) { if ( ! is_array( $term_data ) ) { continue; } ++$report->terms; $this->import_term_field( $report, $dry, $overwrite, (int) $term_id, TermMetaFields::TITLE, $this->convert_variables( (string) ( $term_data['wpseo_title'] ?? '' ) ) ); $this->import_term_field( $report, $dry, $overwrite, (int) $term_id, TermMetaFields::DESCRIPTION, $this->convert_variables( (string) ( $term_data['wpseo_desc'] ?? '' ) ) ); } } } /** * @return int[] */ private function source_post_ids(): array { global $wpdb; $placeholders = implode( ',', array_fill( 0, count( self::POST_META_KEYS ), '%s' ) ); return array_map( 'intval', (array) $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- read-only query against another plugin's own custom table (import dry-run / foreign-plugin conflict detection) — not our schema, WP_Query/get_posts can't reach it. / one-shot read during import/conflict-detection, not a repeated query in a request loop — caching would serve stale data across saves. $wpdb->prepare( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- placeholders string is built from a fixed-size constant array; \$wpdb->prepare() IS used correctly with the values passed as the args array — PCP's static check doesn't trace the placeholder string construction. self::POST_META_KEYS ) ) ); } /** * Converts Yoast's `%%variable%%` syntax to ours; any remaining unknown * `%%...%%` tokens are stripped (they'd otherwise render literally in * titles), with surrounding whitespace collapsed. */ private function convert_variables( string $value ): string { $value = str_replace( array( '%%title%%', '%%sitename%%', '%%sep%%', '%%excerpt%%', '%%term_title%%', '%%currentyear%%' ), array( '%post_title%', '%site_name%', '%sep%', '%post_excerpt%', '%term_title%', '%current_year%' ), $value ); $value = (string) preg_replace( '/%%[a-z0-9_-]+%%/i', '', $value ); return trim( (string) preg_replace( '/\s{2,}/', ' ', $value ) ); } }