variables = new Variable_Converter( '/%%([A-Za-z0-9_\-]+?)%%/', array( 'title' => 'title', 'sitename' => 'sitename', 'sitedesc' => 'sitedesc', 'sep' => 'sep', 'page' => 'page', 'pagenumber' => 'pagenumber', 'pagetotal' => 'pagetotal', 'excerpt' => 'excerpt', 'excerpt_only' => 'excerpt', 'category' => 'categories', 'primary_category' => 'category', 'tag' => 'tag', 'term_title' => 'term', 'term_description' => 'term_description', 'category_title' => 'term', 'category_description' => 'term_description', 'name' => 'author', 'date' => 'date', 'modified' => 'modified', 'currentyear' => 'currentyear', 'currentmonth' => 'currentmonth', 'searchphrase' => 'searchphrase', 'pt_single' => 'post_type', ), static function ( $name ) { if ( 0 === strpos( $name, 'cf_' ) && strlen( $name ) > 3 ) { return '%custom_field(' . substr( $name, 3 ) . ')%'; } if ( 0 === strpos( $name, 'ct_' ) && strlen( $name ) > 3 ) { return '%custom_tax(' . substr( $name, 3 ) . ')%'; } return null; } ); } /** * Source id. * * @since 1.0.0 * * @return string */ public function id(): string { return 'yoast'; } /** * Plugin name. * * @since 1.0.0 * * @return string */ public function label(): string { return 'Yoast SEO'; } /** * Whether Yoast left data. * * @since 1.0.0 * * @return bool */ public function detected(): bool { return false !== get_option( 'wpseo_titles', false ) || Source_Support::count_with_meta( 'postmeta', 'post_id', self::PREFIX ) > 0; } /** * Whether Yoast is active. * * @since 1.0.0 * * @return bool */ public function is_active(): bool { return defined( 'WPSEO_VERSION' ); } /** * What there is to import. * * @since 1.0.0 * * @return array{posts: int, terms: int, authors: int, redirects: int, settings: bool} */ public function counts(): array { $terms = 0; foreach ( $this->term_data() as $entries ) { $terms += count( $entries ); } return array( 'posts' => Source_Support::count_with_meta( 'postmeta', 'post_id', self::PREFIX ), 'terms' => $terms, 'authors' => Source_Support::count_with_meta( 'usermeta', 'user_id', 'wpseo_' ), 'redirects' => 0, 'settings' => false !== get_option( 'wpseo_titles', false ), ); } /** * A batch of posts. * * @since 1.0.0 * * @param int $after Only posts with a higher id. * @param int $limit Most posts to return. * @return array, notes: string[]}> */ public function posts( int $after, int $limit ): array { $ids = Source_Support::ids_with_meta( 'postmeta', 'post_id', self::PREFIX, $after, $limit ); $meta = Source_Support::meta_for( 'postmeta', 'post_id', $ids, self::PREFIX ); $out = array(); foreach ( $ids as $id ) { $out[ $id ] = $this->record( $meta[ $id ] ?? array(), self::PREFIX, true ); $out[ $id ]['id'] = $id; } return $out; } /** * A batch of terms. * * @since 1.0.0 * * @param int $after Only terms with a higher id. * @param int $limit Most terms to return. * @return array, notes: string[]}> */ public function terms( int $after, int $limit ): array { $all = array(); foreach ( $this->term_data() as $entries ) { foreach ( $entries as $term_id => $values ) { if ( (int) $term_id > $after && is_array( $values ) ) { $all[ (int) $term_id ] = $values; } } } ksort( $all ); $out = array(); foreach ( array_slice( $all, 0, $limit, true ) as $term_id => $values ) { $prefixed = array(); foreach ( $values as $key => $value ) { $name = 0 === strpos( (string) $key, 'wpseo_' ) ? substr( (string) $key, 6 ) : (string) $key; $prefixed[ 'desc' === $name ? 'metadesc' : $name ] = is_scalar( $value ) ? (string) $value : ''; } $out[ $term_id ] = $this->record( $prefixed, '', false, $this->term_robots( $prefixed ) ); $out[ $term_id ]['id'] = $term_id; } return $out; } /** * A batch of authors. * * @since 1.0.0 * * @param int $after Only users with a higher id. * @param int $limit Most users to return. * @return array, notes: string[]}> */ public function authors( int $after, int $limit ): array { $ids = Source_Support::ids_with_meta( 'usermeta', 'user_id', 'wpseo_', $after, $limit ); $meta = Source_Support::meta_for( 'usermeta', 'user_id', $ids, 'wpseo_' ); $out = array(); foreach ( $ids as $id ) { $values = $meta[ $id ] ?? array(); $notes = array(); $title = $this->template( $values['wpseo_title'] ?? '', $notes ); $desc = $this->template( $values['wpseo_metadesc'] ?? '', $notes ); $out[ $id ] = array( 'id' => $id, 'fields' => Source_Support::filled( array( Keys::TITLE => $title, Keys::DESCRIPTION => $desc, Keys::ROBOTS => 'on' === ( $values['wpseo_noindex_author'] ?? '' ) ? array( 'noindex' => true ) : array(), ) ), 'notes' => array_values( $notes ), ); } return $out; } /** * The site-wide settings. * * @since 1.0.0 * * @return array{settings: array>, notes: string[]} */ public function settings(): array { $titles = get_option( 'wpseo_titles', array() ); $social = get_option( 'wpseo_social', array() ); $titles = is_array( $titles ) ? $titles : array(); $social = is_array( $social ) ? $social : array(); $notes = array(); $out = array( 'titles' => array(), 'social' => array(), 'profile' => array(), ); $separators = array( 'sc-dash' => '-', 'sc-ndash' => '–', 'sc-mdash' => '—', 'sc-pipe' => '|', 'sc-middot' => '·', 'sc-bull' => '•', 'sc-tilde' => '~', 'sc-raquo' => '»', 'sc-laquo' => '«', ); if ( isset( $titles['separator'], $separators[ $titles['separator'] ] ) ) { $out['titles']['separator'] = $separators[ $titles['separator'] ]; } $out['titles']['home_title'] = $this->template( (string) ( $titles['title-home-wpseo'] ?? '' ), $notes ); $out['titles']['home_description'] = $this->template( (string) ( $titles['metadesc-home-wpseo'] ?? '' ), $notes ); $out['titles']['author_title'] = $this->template( (string) ( $titles['title-author-wpseo'] ?? '' ), $notes ); $out['titles']['author_description'] = $this->template( (string) ( $titles['metadesc-author-wpseo'] ?? '' ), $notes ); $out['titles']['date_title'] = $this->template( (string) ( $titles['title-archive-wpseo'] ?? '' ), $notes ); $out['titles']['date_description'] = $this->template( (string) ( $titles['metadesc-archive-wpseo'] ?? '' ), $notes ); $out['titles']['search_title'] = $this->template( (string) ( $titles['title-search-wpseo'] ?? '' ), $notes ); $out['titles']['not_found_title'] = $this->template( (string) ( $titles['title-404-wpseo'] ?? '' ), $notes ); foreach ( array( 'author_noindex' => 'noindex-author-wpseo', 'date_noindex' => 'noindex-archive-wpseo', ) as $target => $key ) { if ( array_key_exists( $key, $titles ) ) { $out['titles'][ $target ] = (bool) $titles[ $key ]; } } if ( array_key_exists( 'noindex-author-noposts-wpseo', $titles ) ) { $out['titles']['author_noindex_single_author'] = (bool) $titles['noindex-author-noposts-wpseo']; } foreach ( get_post_types( array( 'public' => true ) ) as $type ) { $entry = $this->entry( $titles, $type, '', $notes ); if ( $entry ) { $out['titles']['post_types'][ $type ] = $entry; } } foreach ( get_taxonomies( array( 'public' => true ) ) as $taxonomy ) { $entry = $this->entry( $titles, $taxonomy, 'tax-', $notes ); if ( $entry ) { $out['titles']['taxonomies'][ $taxonomy ] = $entry; } } if ( ! empty( $social['twitter_site'] ) ) { $out['social']['twitter_site'] = ltrim( (string) $social['twitter_site'], '@' ); $out['profile']['x'] = 'https://x.com/' . rawurlencode( $out['social']['twitter_site'] ); } if ( isset( $social['twitter_card_type'] ) && in_array( $social['twitter_card_type'], array( 'summary', 'summary_large_image' ), true ) ) { $out['social']['twitter_card_type'] = $social['twitter_card_type']; } $image = Source_Support::image_id( $social['og_default_image_id'] ?? 0, (string) ( $social['og_default_image'] ?? '' ) ); if ( $image > 0 ) { $out['social']['default_image_id'] = $image; } foreach ( array( 'facebook_site' => 'facebook', 'instagram_url' => 'instagram', 'linkedin_url' => 'linkedin', 'youtube_url' => 'youtube', 'pinterest_url' => 'pinterest', ) as $key => $target ) { if ( ! empty( $social[ $key ] ) ) { $out['profile'][ $target ] = (string) $social[ $key ]; } } $person = 'person' === ( $titles['company_or_person'] ?? '' ); $out['profile']['entity_type'] = $person ? 'person' : 'organization'; $name = $person ? ( $titles['company_or_person_user_id'] ?? '' ) : ( $titles['company_name'] ?? '' ); if ( $person && is_numeric( $name ) ) { $user = get_userdata( (int) $name ); $name = $user ? $user->display_name : ''; } $out['profile']['entity_name'] = is_string( $name ) ? $name : ''; if ( ! $person ) { $logo = Source_Support::image_id( $titles['company_logo_id'] ?? 0, (string) ( $titles['company_logo'] ?? '' ) ); if ( $logo > 0 ) { $out['profile']['entity_logo_id'] = $logo; } } return array( 'settings' => $out, 'notes' => array_values( $notes ), ); } /** * Yoast keeps its redirects in a paid add-on, so there is nothing to read. * * @since 1.0.0 * * @param int $after Unused. * @param int $limit Unused. * @return array>, notes: string[]}> */ public function redirects( int $after, int $limit ): array { return array(); } /** * Turns Yoast's values for one object into a record. * * @since 1.0.0 * * @param array $meta Values by key. * @param string $prefix Key prefix to strip (post meta only). * @param bool $is_post Whether the keys are post meta keys. * @param array $robots Robots flags worked out by the caller (terms). * @return array{id: int, fields: array, notes: string[]} */ private function record( array $meta, string $prefix, bool $is_post, array $robots = array() ): array { $values = array(); foreach ( $meta as $key => $value ) { $values[ '' !== $prefix && 0 === strpos( $key, $prefix ) ? substr( $key, strlen( $prefix ) ) : $key ] = $value; } $notes = array(); $get = static function ( $name ) use ( $values ) { return isset( $values[ $name ] ) ? trim( $values[ $name ] ) : ''; }; if ( $is_post ) { $robots = array(); if ( '1' === $get( 'meta-robots-noindex' ) ) { $robots['noindex'] = true; } elseif ( '2' === $get( 'meta-robots-noindex' ) ) { $robots['noindex'] = false; } if ( '1' === $get( 'meta-robots-nofollow' ) ) { $robots['nofollow'] = true; } foreach ( explode( ',', $get( 'meta-robots-adv' ) ) as $flag ) { $flag = trim( $flag ); if ( in_array( $flag, array( 'noimageindex', 'noarchive', 'nosnippet' ), true ) ) { $robots[ $flag ] = true; } } } $fields = array( Keys::TITLE => $this->template( $get( 'title' ), $notes ), Keys::DESCRIPTION => $this->template( $get( 'metadesc' ), $notes ), Keys::FOCUS_KEYWORDS => '' !== $get( 'focuskw' ) ? Source_Support::keywords( $get( 'focuskw' ) ) : array(), Keys::CANONICAL => esc_url_raw( $get( 'canonical' ) ), Keys::ROBOTS => $robots, Keys::BREADCRUMB_TITLE => sanitize_text_field( $get( 'bctitle' ) ), Keys::OG_TITLE => $this->template( $get( 'opengraph-title' ), $notes ), Keys::OG_DESCRIPTION => $this->template( $get( 'opengraph-description' ), $notes ), Keys::OG_IMAGE => Source_Support::image_id( $get( 'opengraph-image-id' ), $get( 'opengraph-image' ) ), Keys::TWITTER_TITLE => $this->template( $get( 'twitter-title' ), $notes ), Keys::TWITTER_DESCRIPTION => $this->template( $get( 'twitter-description' ), $notes ), Keys::TWITTER_IMAGE => Source_Support::image_id( $get( 'twitter-image-id' ), $get( 'twitter-image' ) ), ); if ( $is_post && '1' === $get( 'is_cornerstone' ) ) { $fields[ Keys::PILLAR ] = true; } return array( 'id' => 0, 'fields' => Source_Support::filled( $fields ), 'notes' => array_values( $notes ), ); } /** * Robots flags of a term entry. * * @since 1.0.0 * * @param array $values Term values with `wpseo_` removed from the keys. * @return array */ private function term_robots( array $values ): array { $noindex = $values['noindex'] ?? ''; if ( 'noindex' === $noindex ) { return array( 'noindex' => true ); } return 'index' === $noindex ? array( 'noindex' => false ) : array(); } /** * Converts a template and remembers what could not be converted. * * @since 1.0.0 * * @param string $template Yoast template. * @param string[] $notes Notes to add to. * @return string */ private function template( string $template, array &$notes ): string { $converted = $this->variables->convert( $template ); foreach ( $converted['unsupported'] as $name ) { $notes[ $name ] = $name; } return $converted['text']; } /** * Title, description and noindex of a post type or taxonomy. * * @since 1.0.0 * * @param array $titles The `wpseo_titles` option. * @param string $name Post type or taxonomy name. * @param string $infix `tax-` for taxonomies, empty for post types. * @param string[] $notes Notes to add to. * @return array */ private function entry( array $titles, string $name, string $infix, array &$notes ): array { $entry = array( 'title' => $this->template( (string) ( $titles[ 'title-' . $infix . $name ] ?? '' ), $notes ), 'description' => $this->template( (string) ( $titles[ 'metadesc-' . $infix . $name ] ?? '' ), $notes ), ); if ( array_key_exists( 'noindex-' . $infix . $name, $titles ) ) { $entry['noindex'] = (bool) $titles[ 'noindex-' . $infix . $name ]; } return array_filter( $entry, static function ( $value ) { return '' !== $value; } ); } /** * Yoast's term data: taxonomy => term id => values. * * @since 1.0.0 * * @return array> */ private function term_data(): array { $data = get_option( 'wpseo_taxonomy_meta', array() ); return is_array( $data ) ? $data : array(); } }