File "importer-utils.php"

Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/inc/lib/background-process/importer-utils.php
File size: 16.67 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * Importer Utilities
 *
 * Shared helper functions for all importer classes.
 *
 * @package SureRank\Inc\Importers
 * @since   1.1.0
 */

namespace SureRank\Inc\Importers;

use Exception;
use SureRank\Inc\Admin\Update_Timestamp;
use SureRank\Inc\API\Admin;
use SureRank\Inc\API\Post;
use SureRank\Inc\API\Term;
use SureRank\Inc\Functions\Helper;
use SureRank\Inc\Functions\Settings;
use SureRank\Inc\Functions\Update;
use SureRank\Inc\Schema\Helper as SchemaHelper;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class ImporterUtils
 *
 * Provides shared functionality (response builder, metadata persistence) for importers.
 */
final class ImporterUtils {

	/**
	 * Build a standard response array.
	 *
	 * @param string $message Human-readable message.
	 * @param bool   $success Whether the operation succeeded.
	 * @param mixed  $data    Optional additional data to include in the response.
	 * @param bool   $no_data_found Whether no data was found during the operation.
	 * @phpstan-return array{success: bool, message: string, data: mixed}
	 */
	public static function build_response( string $message, bool $success = true, $data = [], $no_data_found = false ): array {
		return [
			'success'       => $success,
			'message'       => $message,
			'data'          => $data,
			'no_data_found' => $no_data_found,
		];
	}

	/**
	 * Update SureRank post_meta for a given post.
	 *
	 * Replaces implementation of update_post_meta_data in each importer.
	 *
	 * @param int                  $post_id Post ID.
	 * @param array<string, mixed> $data    Prepared SureRank meta values to write.
	 * @phpstan-return array{success: bool, message: string}
	 */
	public static function update_post_meta_data( int $post_id, array $data ): array {
		try {
			Post::update_post_meta_common( $post_id, $data );

			// Mark this post as having completed migration.
			self::update_surerank_migrated( $post_id );

			return self::build_response(
				sprintf(
					/* translators: %s: post ID */
					__( 'Successfully updated SureRank settings for post ID %s.', 'surerank' ),
					$post_id
				)
			);
		} catch ( Exception $e ) {
			return self::build_response(
				sprintf(
					/* translators: 1: post ID, 2: error message */
					__( 'Failed to update SureRank settings for post ID %1$s. Error: %2$s', 'surerank' ),
					$post_id,
					$e->getMessage()
				),
				false
			);
		}
	}

	/**
	 * Update SureRank term_meta for a given term.
	 *
	 * Replaces implementation of update_term_meta_data in each importer.
	 *
	 * @param int                  $term_id Term ID.
	 * @param array<string, mixed> $data    Prepared SureRank meta values to write.
	 * @phpstan-return array{success: bool, message: string}
	 */
	public static function update_term_meta_data( int $term_id, array $data ): array {
		try {

			Term::update_term_meta_common( $term_id, $data );
			self::update_surerank_migrated( $term_id, false );

			return self::build_response(
				sprintf(
					/* translators: %s: term ID */
					__( 'Successfully updated SureRank settings for term ID %s.', 'surerank' ),
					$term_id
				)
			);
		} catch ( Exception $e ) {
			return self::build_response(
				sprintf(
					/* translators: 1: term ID, 2: error message */
					__( 'Failed to update SureRank settings for term ID %1$s. Error: %2$s', 'surerank' ),
					$term_id,
					$e->getMessage()
				),
				false
			);
		}
	}

	/**
	 * Update global settings.
	 *
	 * @param array<string, mixed> $data Data to update.
	 * @return void
	 */
	public static function update_global_settings( $data ) {
		$db_options = Settings::get();

		$updated_options = Admin::get_instance()->get_updated_options( $data, $db_options );

		Helper::update_flush_rules( $updated_options );

		if ( is_array( $updated_options ) && array_intersect( [ 'social_profiles', 'facebook_page_url', 'twitter_profile_username' ], $updated_options ) ) {
			$data['social_profiles']['facebook'] = $data['facebook_page_url'] ?? '';

			if ( ! empty( $data['twitter_profile_username'] ) ) {
				$data['social_profiles']['twitter'] = str_replace( '@', '', $data['twitter_profile_username'] );
			}

			Admin::get_instance()->process_onboarding_data( $data, $data );
		}

		$data = array_merge( $db_options, $data );

		if ( Update::option( SURERANK_SETTINGS, $data ) ) {
			// Update global timestamp.
			Update_Timestamp::timestamp_option();
		}
	}

	/**
	 * Update SureRank migration status for a term.
	 *
	 * @param int  $id Term ID.
	 * @param bool $is_post Whether the ID refers to a post (true) or term (false).
	 * @return void
	 */
	public static function update_surerank_migrated( $id, $is_post = true ) {

		if ( $is_post ) {
			Update::post_meta( $id, 'surerank_migration', 1 );
			return;
		}

		Update::term_meta( $id, 'surerank_migration', 1 );
	}

	/**
	 * Map social profiles from Rank Math to SureRank format.
	 *
	 * @param string|array<string> $social_profiles Social profiles from Rank Math.
	 * @param array<string,string> $surerank_social_profiles SureRank social profiles mapping.
	 * @return array<string,string> Mapped social profiles.
	 */
	public static function get_mapped_social_profiles( $social_profiles, $surerank_social_profiles ) {

		if ( empty( $social_profiles ) ) {
			return $surerank_social_profiles;
		}

		$decoded_profiles = [];

		if ( is_string( $social_profiles ) ) {
			$decoded_profiles = array_filter( array_map( 'trim', explode( "\n", $social_profiles ) ) );
			$decoded_profiles = array_values( $decoded_profiles );
		} elseif ( is_array( $social_profiles ) ) {
			$decoded_profiles = $social_profiles;
		}
		foreach ( $surerank_social_profiles as $key => $value ) {

			foreach ( $decoded_profiles as $profile ) {
				// Check if $key is a substring of the profile URL (case-insensitive if needed).
				if ( stripos( $profile, $key ) !== false ) {
					$surerank_social_profiles[ $key ] = $profile;
					break; // Found the first matching profile, no need to continue.
				}
				// Special handling for WhatsApp, Telegram, and Bluesky.
				if ( self::is_special_social_platform( $profile, $key ) ) {
					$surerank_social_profiles[ $key ] = $profile;
					break;
				}
			}
		}

		return $surerank_social_profiles;
	}

	/**
	 * Get the taxonomies that should be excluded from the import.
	 *
	 * @return array<string, \WP_Taxonomy> Array of taxonomy objects.
	 * @since 1.1.0
	 */
	public static function get_excluded_taxonomies() {
		$taxonomies_objects = get_taxonomies(
			[ 'public' => true ],
			'objects'
		);

		$unsupported = SchemaHelper::UNSUPPORTED_TAXONOMIES;
		foreach ( $unsupported as $slug ) {
			if ( isset( $taxonomies_objects[ $slug ] ) ) {
				unset( $taxonomies_objects[ $slug ] );
			}
		}

		return $taxonomies_objects;
	}

	/**
	 * Prepares placeholders and parameters for SQL queries.
	 *
	 * @param array<string> $items Items for placeholders (e.g., post types or taxonomies).
	 * @param array<string> $excluded_keys Excluded meta keys.
	 * @param string        $meta_prefix Meta key prefix.
	 * @param int|null      $batch_size Batch size for pagination (null for count queries).
	 * @param int|null      $offset Offset for pagination (null for count queries).
	 * @return array{placeholders: array<string>, params: array<int|string>} Placeholders and parameters.
	 */
	public static function prepare_query_params( array $items, array $excluded_keys, string $meta_prefix, ?int $batch_size = null, ?int $offset = null ): array {
		$placeholders = [];
		$params       = array_merge(
			[ 'surerank_migration', $meta_prefix ],
			$excluded_keys,
			$items
		);

		// Create placeholders for items and excluded keys.
		$placeholders[] = implode( ',', array_fill( 0, count( $excluded_keys ), '%s' ) ); // For excluded keys.
		$placeholders[] = implode( ',', array_fill( 0, count( $items ), '%s' ) ); // For items.

		if ( $batch_size !== null && $offset !== null ) {
			$params[] = $batch_size;
			$params[] = $offset;
		}

		return [
			'placeholders' => $placeholders,
			'params'       => $params,
		];
	}

	/**
	 * Get total count of posts for pagination.
	 *
	 * @param array<string> $post_types Array of post type names.
	 * @param array<string> $excluded_keys Array of excluded meta keys.
	 * @param string        $meta_prefix Meta key prefix.
	 * @return int Total number of posts.
	 */
	public static function get_total_posts_count( array $post_types, array $excluded_keys, string $meta_prefix ): int {
		global $wpdb;

		/**
		 * Prepares query parameters for post migration.
		 *
		 * This method sets up placeholders for database query to find posts that:
		 * - Have no 'surerank_migration' meta key
		 * - Have at least one meta key matching the prefix
		 * - Are not in the excluded keys list
		 * - Belong to specified post types
		 *
		 * The query is structured to:
		 * 1. Create placeholders for excluded keys
		 * 2. Create placeholders for post types
		 * 3. Filter by main meta key 'surerank_migration'
		 * 4. Filter by secondary meta keys using prefix matching
		 * 5. Count distinct post IDs meeting these criteria
		 *
		 * This is used for identifying posts that need migration in the WordPress database.
		 */
		$query_data = self::prepare_query_params( $post_types, $excluded_keys, $meta_prefix );

		$sql = "SELECT COUNT(DISTINCT p.ID)
                FROM {$wpdb->posts} p
                LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    AND pm.meta_key = %s
                INNER JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
                    AND pm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND pm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE p.post_type IN ({$query_data['placeholders'][1]})
                 AND p.post_status != 'auto-draft'
                 AND pm.meta_id IS NULL";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return (int) $wpdb->get_var($wpdb->prepare($sql, $query_data['params'])); // phpcs:ignore
	}

	/**
	 * Get post IDs for the given parameters.
	 *
	 * @param array<string> $post_types Array of post type names.
	 * @param array<string> $excluded_keys Array of excluded meta keys.
	 * @param string        $meta_prefix Meta key prefix.
	 * @param int           $batch_size Number of posts to retrieve.
	 * @param int           $offset Offset for pagination.
	 * @return array<int> Array of post IDs.
	 */
	public static function get_post_ids( array $post_types, array $excluded_keys, string $meta_prefix, int $batch_size, int $offset ): array {
		global $wpdb;

		$query_data = self::prepare_query_params( $post_types, $excluded_keys, $meta_prefix, $batch_size, $offset );

		$sql = "SELECT DISTINCT p.ID
                FROM {$wpdb->posts} p
                LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    AND pm.meta_key = %s
                INNER JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
                    AND pm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND pm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE p.post_type IN ({$query_data['placeholders'][1]})
                 AND p.post_status != 'auto-draft'
                 AND pm.meta_id IS NULL
                 ORDER BY p.ID
                 LIMIT %d OFFSET %d";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_col($wpdb->prepare($sql, $query_data['params'])); // phpcs:ignore
	}

	/**
	 * Get total count of terms for pagination.
	 *
	 * @param array<string> $taxonomies Array of taxonomy names.
	 * @param array<string> $excluded_keys Array of excluded meta keys.
	 * @param string        $meta_prefix Meta key prefix.
	 * @return int Total number of terms.
	 */
	public static function get_total_terms_count( array $taxonomies, array $excluded_keys, string $meta_prefix ): int {
		global $wpdb;

		$query_data = self::prepare_query_params( $taxonomies, $excluded_keys, $meta_prefix );

		$sql = "SELECT COUNT(DISTINCT t.term_id)
                FROM {$wpdb->terms} t
                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                LEFT JOIN {$wpdb->termmeta} tm ON t.term_id = tm.term_id
                    AND tm.meta_key = %s
                INNER JOIN {$wpdb->termmeta} tm2 ON t.term_id = tm2.term_id
                    AND tm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND tm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE tt.taxonomy IN ({$query_data['placeholders'][1]})
                 AND tm.meta_id IS NULL";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return (int) $wpdb->get_var($wpdb->prepare($sql, $query_data['params'])); // phpcs:ignore
	}

	/**
	 * Get term IDs for the given parameters.
	 *
	 * @param array<string> $taxonomies Array of taxonomy names.
	 * @param array<string> $excluded_keys Array of excluded meta keys.
	 * @param string        $meta_prefix Meta key prefix.
	 * @param int           $batch_size Number of terms to retrieve.
	 * @param int           $offset Offset for pagination.
	 * @return array<int> Array of term IDs.
	 */
	public static function get_term_ids( array $taxonomies, array $excluded_keys, string $meta_prefix, int $batch_size, int $offset ): array {
		global $wpdb;

		/**
		 * Prepares query parameters for term migration.
		 *
		 * This method sets up placeholders for database query to find terms that:
		 * - Have no 'surerank_migration' meta key
		 * - Have at least one meta key matching the prefix
		 * - Are not in the excluded keys list
		 * - Belong to specified taxonomies
		 *
		 * The query is structured to:
		 * 1. Create placeholders for excluded keys
		 * 2. Create placeholders for taxonomies
		 * 3. Filter by main meta key 'surerank_migration'
		 * 4. Filter by secondary meta keys using prefix matching
		 * 5. Select distinct term IDs meeting these criteria
		 *
		 * This is used for identifying terms that need migration in the WordPress database.
		 */

		$query_data = self::prepare_query_params( $taxonomies, $excluded_keys, $meta_prefix, $batch_size, $offset );

		$sql = "SELECT DISTINCT t.term_id
                FROM {$wpdb->terms} t
                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                LEFT JOIN {$wpdb->termmeta} tm ON t.term_id = tm.term_id
                    AND tm.meta_key = %s
                INNER JOIN {$wpdb->termmeta} tm2 ON t.term_id = tm2.term_id
                    AND tm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND tm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE tt.taxonomy IN ({$query_data['placeholders'][1]})
                 AND tm.meta_id IS NULL
                 ORDER BY t.term_id
                 LIMIT %d OFFSET %d";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_col($wpdb->prepare($sql, $query_data['params'])); // phpcs:ignore
	}

	/**
	 * Check if the given type is valid.
	 *
	 * @param string                     $type The type to check.
	 * @param array<string>              $not_valid_types List of invalid types.
	 * @param array<string|\WP_Taxonomy> $public_types List of public types.
	 * @param bool                       $is_taxonomy Whether the type is a taxonomy.
	 * @return bool True if valid, false otherwise.
	 */
	public static function is_type_valid( $type, $not_valid_types = [], $public_types = [], $is_taxonomy = false ) {
		if ( $type === '' || empty( $type ) ) {
			return false;
		}

		if ( in_array( $type, $not_valid_types, true ) ) {
			return false;
		}

		if ( $is_taxonomy && ! isset( $public_types[ $type ] ) ) {
			return false;
		}

		if ( ! $is_taxonomy && ! in_array( $type, $public_types, true ) ) {
			return false;
		}
		return true;
	}

	/**
	 * Return a response indicating an invalid post type.
	 *
	 * @param int  $post_id The ID of the post or term.
	 * @param bool $is_taxonomy Whether the ID refers to a taxonomy (true) or post (false).
	 * @return array{success: bool, message: string}
	 */
	public static function not_valid_response( $post_id, $is_taxonomy = false ): array {
		$message = $is_taxonomy
			? sprintf(
				/* translators: %d: term ID */
				__( 'Invalid Term ID %d.', 'surerank' ),
				$post_id
			)
			: sprintf(
				/* translators: %d: post ID */
				__( 'Invalid Post ID %d.', 'surerank' ),
				$post_id
			);

		return self::build_response(
			$message,
			false,
			[],
			true
		);
	}

	/**
	 * Check if a profile URL matches a specific social platform.
	 *
	 * @param string $profile The profile URL to check.
	 * @param string $key The social platform key.
	 * @return bool True if the profile matches the platform.
	 */
	private static function is_special_social_platform( string $profile, string $key ): bool {
		$platform_patterns = [
			'whatsapp' => 'https://wa.me/',
			'telegram' => 'https://t.me/',
			'bluesky'  => 'https://bsky.app/',
		];

		return isset( $platform_patterns[ $key ] ) && stripos( $profile, $platform_patterns[ $key ] ) !== false;
	}
}