: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Indexable_To_Postmeta_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Replacevar_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Provider_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Robots_Transformer_Service;
use Yoast\WP\SEO\Services\Importing\Aioseo\Aioseo_Social_Images_Provider_Service;
* Importing action for AIOSEO post data.
class Aioseo_Posts_Importing_Action extends Abstract_Aioseo_Importing_Action {
* The plugin of the action.
public const PLUGIN = 'aioseo';
* The type of the action.
public const TYPE = 'posts';
* The map of aioseo to yoast meta.
protected $aioseo_to_yoast_map = [
'transform_method' => 'simple_import_post',
'yoast_name' => 'description',
'transform_method' => 'simple_import_post',
'yoast_name' => 'open_graph_title',
'transform_method' => 'simple_import_post',
'yoast_name' => 'open_graph_description',
'transform_method' => 'simple_import_post',
'yoast_name' => 'twitter_title',
'transform_method' => 'simple_import_post',
'twitter_import' => true,
'twitter_description' => [
'yoast_name' => 'twitter_description',
'transform_method' => 'simple_import_post',
'twitter_import' => true,
'yoast_name' => 'canonical',
'transform_method' => 'url_import_post',
'yoast_name' => 'primary_focus_keyword',
'transform_method' => 'keyphrase_import',
'yoast_name' => 'open_graph_image',
'social_image_import' => true,
'social_setting_prefix_aioseo' => 'og_',
'social_setting_prefix_yoast' => 'open_graph_',
'transform_method' => 'social_image_url_import',
'yoast_name' => 'twitter_image',
'social_image_import' => true,
'social_setting_prefix_aioseo' => 'twitter_',
'social_setting_prefix_yoast' => 'twitter_',
'transform_method' => 'social_image_url_import',
'yoast_name' => 'is_robots_noindex',
'transform_method' => 'post_robots_noindex_import',
'yoast_name' => 'is_robots_nofollow',
'transform_method' => 'post_general_robots_import',
'robot_type' => 'nofollow',
'yoast_name' => 'is_robots_noarchive',
'transform_method' => 'post_general_robots_import',
'robot_type' => 'noarchive',
'yoast_name' => 'is_robots_nosnippet',
'transform_method' => 'post_general_robots_import',
'robot_type' => 'nosnippet',
'robots_noimageindex' => [
'yoast_name' => 'is_robots_noimageindex',
'transform_method' => 'post_general_robots_import',
'robot_type' => 'noimageindex',
* Represents the indexables repository.
* @var Indexable_Repository
protected $indexable_repository;
* The WordPress database instance.
* The indexable_to_postmeta helper.
* @var Indexable_To_Postmeta_Helper
protected $indexable_to_postmeta;
protected $indexable_helper;
* The social images provider service.
* @var Aioseo_Social_Images_Provider_Service
protected $social_images_provider;
* @param Indexable_Repository $indexable_repository The indexables repository.
* @param wpdb $wpdb The WordPress database instance.
* @param Import_Cursor_Helper $import_cursor The import cursor helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper.
* @param Options_Helper $options The options helper.
* @param Image_Helper $image The image helper.
* @param Sanitization_Helper $sanitization The sanitization helper.
* @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler.
* @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service.
* @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service.
* @param Aioseo_Social_Images_Provider_Service $social_images_provider The social images provider service.
public function __construct(
Indexable_Repository $indexable_repository,
Import_Cursor_Helper $import_cursor,
Indexable_Helper $indexable_helper,
Indexable_To_Postmeta_Helper $indexable_to_postmeta,
Sanitization_Helper $sanitization,
Aioseo_Replacevar_Service $replacevar_handler,
Aioseo_Robots_Provider_Service $robots_provider,
Aioseo_Robots_Transformer_Service $robots_transformer,
Aioseo_Social_Images_Provider_Service $social_images_provider
parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );
$this->indexable_repository = $indexable_repository;
$this->indexable_helper = $indexable_helper;
$this->indexable_to_postmeta = $indexable_to_postmeta;
$this->social_images_provider = $social_images_provider;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared.
* Returns the total number of unimported objects.
* @return int The total number of unimported objects.
public function get_total_unindexed() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
$number_of_indexables_to_create = \count( $indexables_to_create );
$completed = $number_of_indexables_to_create === 0;
$this->set_completed( $completed );
return $number_of_indexables_to_create;
* Returns the limited number of unimported objects.
* @param int $limit The maximum number of unimported objects to be returned.
* @return int|false The limited number of unindexed posts. False if the query fails.
public function get_limited_unindexed_count( $limit ) {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
$indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) );
$number_of_indexables_to_create = \count( $indexables_to_create );
$completed = $number_of_indexables_to_create === 0;
$this->set_completed( $completed );
return $number_of_indexables_to_create;
* Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta.
* @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
public function index() {
if ( ! $this->aioseo_helper->aioseo_exists() ) {
$limit = $this->get_limit();
$aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), \ARRAY_A );
$created_indexables = [];
$completed = \count( $aioseo_indexables ) === 0;
$this->set_completed( $completed );
// Let's build the list of fields to check their defaults, to identify whether we're gonna import AIOSEO data in the indexable or not.
$check_defaults_fields = [];
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
// We don't want to check all the imported fields.
if ( ! \in_array( $yoast_mapping['yoast_name'], [ 'open_graph_image', 'twitter_image' ], true ) ) {
$check_defaults_fields[] = $yoast_mapping['yoast_name'];
$last_indexed_aioseo_id = 0;
foreach ( $aioseo_indexables as $aioseo_indexable ) {
$last_indexed_aioseo_id = $aioseo_indexable['id'];
$indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' );
// Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order).
if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) {
if ( $this->indexable_helper->check_if_default_indexable( $indexable, $check_defaults_fields ) ) {
$indexable = $this->map( $indexable, $aioseo_indexable );
$this->indexable_helper->save_indexable( $indexable );
// To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too.
$this->indexable_to_postmeta->map_to_postmeta( $indexable );
$last_indexed_aioseo_id = $aioseo_indexable['id'];
$created_indexables[] = $indexable;
$cursor_id = $this->get_cursor_id();
$this->import_cursor->set_cursor( $cursor_id, $last_indexed_aioseo_id );
return $created_indexables;
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
* Maps AIOSEO meta data to Yoast meta data.
* @param Indexable $indexable The Yoast indexable.
* @param array $aioseo_indexable The AIOSEO indexable.
* @return Indexable The created indexables.
public function map( $indexable, $aioseo_indexable ) {
foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) {
if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) {
$yoast_mapping['subtype'] = $indexable->object_sub_type;
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
// For social images, like open graph and twitter image.
if ( isset( $yoast_mapping['social_image_import'] ) && $yoast_mapping['social_image_import'] ) {
$image_url = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
// Update the indexable's social image only where there's actually a url to import, so as not to lose the social images that we came up with when we originally built the indexable.
if ( ! empty( $image_url ) ) {
$indexable->{$yoast_mapping['yoast_name']} = $image_url;
$image_source_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_source';
$indexable->$image_source_key = 'imported';
$image_id_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_id';
$indexable->$image_id_key = $this->image->get_attachment_by_url( $image_url );
if ( $yoast_mapping['yoast_name'] === 'open_graph_image' ) {
$indexable->open_graph_image_meta = null;
// For twitter import, take the respective open graph data if the appropriate setting is enabled.
if ( isset( $yoast_mapping['twitter_import'] ) && $yoast_mapping['twitter_import'] && $aioseo_indexable['twitter_use_og'] ) {
$aioseo_indexable['twitter_title'] = $aioseo_indexable['og_title'];
$aioseo_indexable['twitter_description'] = $aioseo_indexable['og_description'];
if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) {
$indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
* Transforms the data to be imported.
* @param string $transform_method The method that is going to be used for transforming the data.
* @param array $aioseo_indexable The data of the AIOSEO indexable data that is being imported.
* @param string $aioseo_key The name of the specific set of data that is going to be transformed.
* @param array $yoast_mapping Extra details for the import of the specific data that is going to be transformed.
* @param Indexable $indexable The Yoast indexable that we are going to import the transformed data into.
* @return string|bool|null The transformed data to be imported.
protected function transform_import_data( $transform_method, $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ) {
return \call_user_func( [ $this, $transform_method ], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable );
* Returns the number of objects that will be imported in a single importing pass.
public function get_limit() {
* Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
* @param int $max_posts The maximum number of posts indexed.
$limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 );
if ( ! \is_int( $limit ) || $limit < 1 ) {
* Populates the needed data array based on which columns we use from the AIOSEO indexable table.
* @return array The needed data array that contains all the needed columns.
public function get_needed_data() {
$needed_data = \array_keys( $this->aioseo_to_yoast_map );
\array_push( $needed_data, 'id', 'post_id', 'robots_default', 'og_image_custom_url', 'og_image_type', 'twitter_image_custom_url', 'twitter_image_type', 'twitter_use_og' );
* Populates the needed robot data array to be used in validating against its structure.
* @return array The needed data array that contains all the needed columns.
public function get_needed_robot_data() {
foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) {
if ( isset( $yoast_mapping['robot_type'] ) ) {
$needed_robot_data[] = $yoast_mapping['robot_type'];
return $needed_robot_data;
* Creates a query for gathering AiOSEO data from the database.
* @param int $limit The maximum number of unimported objects to be returned.
* @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too.
* @return string The query to use for importing or counting the number of items to import.
public function query( $limit = false, $just_detect = false ) {
$table = $this->aioseo_helper->get_table();
$select_statement = 'id';
// If we want to import too, we need the actual needed data from AIOSEO indexables.
$needed_data = $this->get_needed_data();
$select_statement = \implode( ', ', $needed_data );
$cursor_id = $this->get_cursor_id();
$cursor = $this->import_cursor->get_cursor( $cursor_id );
* Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor.
* @param int $import_cursor The value of the aioseo post import cursor.
$cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor );
$replacements = [ $cursor ];
if ( ! empty( $limit ) ) {
$replacements[] = $limit;
$limit_statement = ' LIMIT %d';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
return $this->wpdb->prepare(
"SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}",
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
* Minimally transforms data to be imported.
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
* @return string The transformed meta data.
public function simple_import_post( $aioseo_data, $aioseo_key ) {
return $this->simple_import( $aioseo_data[ $aioseo_key ] );
* Transforms URL to be imported.
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with.
* @return string The transformed URL.
public function url_import_post( $aioseo_data, $aioseo_key ) {
return $this->url_import( $aioseo_data[ $aioseo_key ] );
* Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data.
* @param array $aioseo_data All of the AIOSEO data to be imported.
* @param string $aioseo_key The AIOSEO key that contains the setting we're working with, aka keyphrases.
* @return string|null The plucked keyphrase.
public function keyphrase_import( $aioseo_data, $aioseo_key ) {