$this->count_meta( $this->post_meta_keys() ), 'terms' => $this->count_term_meta( $this->term_meta_keys() ), 'redirects' => 0, 'settings' => $this->has_settings(), ); } /** * Post ids this source has data for, from a keyset cursor. Only ids with * data — importing a 50,000-post site from a plugin that wrote 40 values * must cost 40 records, not 50,000. * * @return array */ public function post_ids( int $after, int $limit ): array { return $this->ids_with_meta( $this->post_meta_keys(), $after, $limit, false ); } /** * @return array */ public function term_ids( int $after, int $limit ): array { return $this->ids_with_meta( $this->term_meta_keys(), $after, $limit, true ); } /** * One post's data, normalised. * * @return array */ abstract public function post_record( int $post_id ): array; /** * One term's data, normalised. * * @return array */ public function term_record( int $term_id ): array { return array(); } /** * Site-wide settings, normalised. Keys mirror this plugin's own settings * (see ImportWriter::apply_settings) — separator, home_title, templates, * taxonomy_templates, noindex_types, noindex_taxonomies, author_*, date_*, * search_title, og_default_image, twitter_site, organisation, local. * * @return array */ public function settings(): array { return array(); } /** * Redirects, from a keyset cursor over the source's own rows. * * @return array */ public function redirects( int $after, int $limit ): array { return array(); } // ------------------------------------------------------------- helpers -- /** * The post meta keys that mean "this source has data for this post". * * @return array */ protected function post_meta_keys(): array { return array(); } /** * @return array */ protected function term_meta_keys(): array { return array(); } protected function has_settings(): bool { return false; } /** A meta value as a trimmed string, whatever the source stored. */ protected function meta( int $post_id, string $key, bool $term = false ): string { if ( '' === $key ) { return ''; } $value = $term ? get_term_meta( $post_id, $key, true ) : get_post_meta( $post_id, $key, true ); return is_scalar( $value ) ? trim( (string) $value ) : ''; } /** * Translate the source's template variables into this plugin's. */ protected function vars( string $value ): string { return Variables::translate( $value ); } /** * Robots directives from a value that may be '1', 'yes', 'noindex', an * array, or a comma list — every plugin picked a different one. * * @param mixed $raw * @return array */ protected function robots_from( $raw, string $directive = 'noindex' ): array { $known = array( 'noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex' ); if ( is_array( $raw ) ) { return array_values( array_intersect( $known, array_map( 'strval', $raw ) ) ); } $value = strtolower( trim( (string) $raw ) ); if ( '' === $value ) { return array(); } if ( str_contains( $value, ',' ) ) { $parts = array_map( 'trim', explode( ',', $value ) ); return array_values( array_intersect( $known, $parts ) ); } if ( in_array( $value, $known, true ) ) { return array( $value ); } return in_array( $value, array( '1', 'yes', 'true', 'on' ), true ) ? array( $directive ) : array(); } /** * Drop the keys with nothing in them, so "the source has no value here" * and "the source has an empty value here" cannot be confused downstream. * * @param array $record * @return array */ protected function clean( array $record ): array { foreach ( $record as $key => $value ) { if ( '' === $value || array() === $value || null === $value || false === $value ) { unset( $record[ $key ] ); } if ( 'social' === $key && is_array( $value ) ) { $social = array_filter( array_map( 'strval', $value ), static fn( $v ) => '' !== trim( $v ) ); if ( empty( $social ) ) { unset( $record['social'] ); } else { $record['social'] = $social; } } } return $record; } /** * @param array $keys */ /** * Which meta keys actually hold data, primed once for the whole adapter * fleet. detect() asked fourteen adapters for two COUNTs each, most of * them counting nothing; two grouped queries answer "which keys are * non-empty" for everyone, and only adapters with a live key still run * their precise count. Null means "not primed" — every count stays exact. * * @var array|null */ private static ?array $key_has_data = null; /** The keys this adapter reads, for the fleet-wide prime. @return array{post: array, term: array} */ public function key_map(): array { return array( 'post' => array_values( array_filter( $this->post_meta_keys() ) ), 'term' => array_values( array_filter( $this->term_meta_keys() ) ), ); } /** * @param array $post_keys * @param array $term_keys */ public static function prime_key_data( array $post_keys, array $term_keys ): void { global $wpdb; self::$key_has_data = array(); if ( ! empty( $post_keys ) ) { $in = implode( ',', array_fill( 0, count( $post_keys ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( (array) $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE meta_key IN ({$in}) AND meta_value <> '' AND meta_value <> 'a:0:{}'", ...$post_keys ) ) as $k ) { self::$key_has_data[ 'p:' . $k ] = true; } } if ( ! empty( $term_keys ) ) { $in = implode( ',', array_fill( 0, count( $term_keys ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( (array) $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_key FROM {$wpdb->termmeta} WHERE meta_key IN ({$in}) AND meta_value <> '' AND meta_value <> 'a:0:{}'", ...$term_keys ) ) as $k ) { self::$key_has_data[ 't:' . $k ] = true; } } } /** @param array $keys */ private static function any_primed( string $prefix, array $keys ): ?bool { if ( null === self::$key_has_data ) { return null; } foreach ( $keys as $k ) { if ( ! empty( self::$key_has_data[ $prefix . $k ] ) ) { return true; } } return false; } protected function count_meta( array $keys ): int { global $wpdb; $keys = array_values( array_filter( $keys ) ); if ( empty( $keys ) ) { return 0; } if ( false === self::any_primed( 'p:', $keys ) ) { return 0; } $in = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key IN ({$in}) AND meta_value <> '' AND meta_value <> 'a:0:{}'", ...$keys ) ); } /** * @param array $keys */ protected function count_term_meta( array $keys ): int { global $wpdb; $keys = array_values( array_filter( $keys ) ); if ( empty( $keys ) ) { return 0; } if ( false === self::any_primed( 't:', $keys ) ) { return 0; } $in = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT term_id) FROM {$wpdb->termmeta} WHERE meta_key IN ({$in}) AND meta_value <> ''", ...$keys ) ); } /** * @param array $keys * @return array */ protected function ids_with_meta( array $keys, int $after, int $limit, bool $term ): array { global $wpdb; $keys = array_values( array_filter( $keys ) ); if ( empty( $keys ) ) { return array(); } $table = $term ? $wpdb->termmeta : $wpdb->postmeta; $column = $term ? 'term_id' : 'post_id'; $in = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); // Table and id column travel as identifiers; only the generated %s // list is interpolated. $args = array_merge( array( $column, $table ), $keys, array( $column, $after, $column, max( 1, $limit ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT %i FROM %i WHERE meta_key IN ({$in}) AND meta_value <> '' AND meta_value <> 'a:0:{}' AND %i > %d ORDER BY %i ASC LIMIT %d", ...$args ) ); return array_map( 'intval', (array) $ids ); } /** Does this table exist? Sources that keep their own table ask first. */ protected function table_exists( string $table ): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table; } /** * An option as an array, whatever the source left there. * * @return array */ protected function option( string $name ): array { $value = get_option( $name, array() ); return is_array( $value ) ? $value : array(); } }