shell bypass 403

GrazzMean Shell

: /proc/thread-self/root/proc/self/cwd/ [ drwxr-xr-x ]
Uname: Linux web3.us.cloudlogin.co 5.10.226-xeon-hst #2 SMP Fri Sep 13 12:28:44 UTC 2024 x86_64
Software: Apache
PHP version: 8.1.31 [ PHP INFO ] PHP os: Linux
Server Ip: 162.210.96.117
Your Ip: 3.17.162.245
User: edustar (269686) | Group: tty (888)
Safe Mode: OFF
Disable Function:
NONE

name : actions.tar
indexing/post-link-indexing-action.php000064400000007532147511254640014100 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;

/**
 * Reindexing action for post link indexables.
 */
class Post_Link_Indexing_Action extends Abstract_Link_Indexing_Action {

	/**
	 * The transient name.
	 *
	 * @var string
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_post_link_count';

	/**
	 * The transient cache key for limited counts.
	 *
	 * @var string
	 */
	public const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * Sets the required helper.
	 *
	 * @required
	 *
	 * @param Post_Type_Helper $post_type_helper The post type helper.
	 *
	 * @return void
	 */
	public function set_helper( Post_Type_Helper $post_type_helper ) {
		$this->post_type_helper = $post_type_helper;
	}

	/**
	 * Returns objects to be indexed.
	 *
	 * @return array Objects to be indexed.
	 */
	protected function get_objects() {
		$query = $this->get_select_query( $this->get_limit() );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
		$posts = $this->wpdb->get_results( $query );

		return \array_map(
			static function ( $post ) {
				return (object) [
					'id'      => (int) $post->ID,
					'type'    => 'post',
					'content' => $post->post_content,
				];
			},
			$posts
		);
	}

	/**
	 * Builds a query for counting the number of unindexed post links.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_count_query() {
		$public_post_types = $this->post_type_helper->get_indexable_post_types();
		$indexable_table   = Model::get_table_name( 'Indexable' );
		$links_table       = Model::get_table_name( 'SEO_Links' );

		// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
		return $this->wpdb->prepare(
			"SELECT COUNT(P.ID)
			FROM {$this->wpdb->posts} AS P
			LEFT JOIN $indexable_table AS I
				ON P.ID = I.object_id
				AND I.link_count IS NOT NULL
				AND I.object_type = 'post'
			LEFT JOIN $links_table AS L
				ON L.post_id = P.ID
				AND L.target_indexable_id IS NULL
				AND L.type = 'internal'
				AND L.target_post_id IS NOT NULL
				AND L.target_post_id != 0
			WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
				AND P.post_status = 'publish'
				AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ')',
			$public_post_types
		);
	}

	/**
	 * Builds a query for selecting the ID's of unindexed post links.
	 *
	 * @param int|false $limit The maximum number of post link IDs to return.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_select_query( $limit = false ) {
		$public_post_types = $this->post_type_helper->get_indexable_post_types();
		$indexable_table   = Model::get_table_name( 'Indexable' );
		$links_table       = Model::get_table_name( 'SEO_Links' );
		$replacements      = $public_post_types;

		$limit_query = '';
		if ( $limit ) {
			$limit_query    = 'LIMIT %d';
			$replacements[] = $limit;
		}

		// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
		return $this->wpdb->prepare(
			"
			SELECT P.ID, P.post_content
			FROM {$this->wpdb->posts} AS P
			LEFT JOIN $indexable_table AS I
				ON P.ID = I.object_id
				AND I.link_count IS NOT NULL
				AND I.object_type = 'post'
			LEFT JOIN $links_table AS L
				ON L.post_id = P.ID
				AND L.target_indexable_id IS NULL
				AND L.type = 'internal'
				AND L.target_post_id IS NOT NULL
				AND L.target_post_id != 0
			WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL )
				AND P.post_status = 'publish'
				AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ")
			$limit_query",
			$replacements
		);
	}
}
indexing/indexing-prepare-action.php000064400000001264147511254640013612 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\SEO\Helpers\Indexing_Helper;

/**
 * Class Indexing_Prepare_Action.
 *
 * Action for preparing the indexing routine.
 */
class Indexing_Prepare_Action {

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * Action for preparing the indexing routine.
	 *
	 * @param Indexing_Helper $indexing_helper The indexing helper.
	 */
	public function __construct(
		Indexing_Helper $indexing_helper
	) {
		$this->indexing_helper = $indexing_helper;
	}

	/**
	 * Prepares the indexing routine.
	 *
	 * @return void
	 */
	public function prepare() {
		$this->indexing_helper->prepare();
	}
}
indexing/abstract-indexing-action.php000064400000006176147511254640013766 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

/**
 * Base class of indexing actions.
 */
abstract class Abstract_Indexing_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {

	/**
	 * The transient name.
	 *
	 * This is a trick to force derived classes to define a transient themselves.
	 *
	 * @var string
	 */
	public const UNINDEXED_COUNT_TRANSIENT = null;

	/**
	 * The transient cache key for limited counts.
	 *
	 * @var string
	 */
	public const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';

	/**
	 * Builds a query for selecting the ID's of unindexed posts.
	 *
	 * @param bool $limit The maximum number of post IDs to return.
	 *
	 * @return string The prepared query string.
	 */
	abstract protected function get_select_query( $limit );

	/**
	 * Builds a query for counting the number of unindexed posts.
	 *
	 * @return string The prepared query string.
	 */
	abstract protected function get_count_query();

	/**
	 * Returns a limited number of unindexed posts.
	 *
	 * @param int $limit Limit the maximum number of unindexed posts that are counted.
	 *
	 * @return int The limited number of unindexed posts. 0 if the query fails.
	 */
	public function get_limited_unindexed_count( $limit ) {
		$transient = \get_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
		if ( $transient !== false ) {
			return (int) $transient;
		}

		\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );

		$query = $this->get_select_query( $limit );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
		$unindexed_object_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query );
		$count                = (int) \count( $unindexed_object_ids );

		\set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) );

		return $count;
	}

	/**
	 * Returns the total number of unindexed posts.
	 *
	 * @return int|false The total number of unindexed posts. False if the query fails.
	 */
	public function get_total_unindexed() {
		$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
		if ( $transient !== false ) {
			return (int) $transient;
		}

		// Store transient before doing the query so multiple requests won't make multiple queries.
		// Only store this for 15 minutes to ensure that if the query doesn't complete a wrong count is not kept too long.
		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) );

		$query = $this->get_count_query();

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query.
		$count = ( $query === '' ) ? 0 : $this->wpdb->get_var( $query );

		if ( \is_null( $count ) ) {
			return false;
		}

		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $count, \DAY_IN_SECONDS );

		/**
		 * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
		 *
		 * @internal
		 */
		\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $count );

		return (int) $count;
	}
}
indexing/indexable-general-indexation-action.php000064400000007601147511254640016060 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * General reindexing action for indexables.
 */
class Indexable_General_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {

	/**
	 * The transient cache key.
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_general_items';

	/**
	 * Represents the indexables repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Indexable_General_Indexation_Action constructor.
	 *
	 * @param Indexable_Repository $indexable_repository The indexables repository.
	 */
	public function __construct( Indexable_Repository $indexable_repository ) {
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_total_unindexed() {
		$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
		if ( $transient !== false ) {
			return (int) $transient;
		}

		$indexables_to_create = $this->query();

		$result = \count( $indexables_to_create );

		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );

		/**
		 * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
		 *
		 * @internal
		 */
		\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );

		return $result;
	}

	/**
	 * Returns a limited number of unindexed posts.
	 *
	 * @param int $limit Limit the maximum number of unindexed posts that are counted.
	 *
	 * @return int|false The limited number of unindexed posts. False if the query fails.
	 */
	public function get_limited_unindexed_count( $limit ) {
		return $this->get_total_unindexed();
	}

	/**
	 * Creates indexables for unindexed system pages, the date archive, and the homepage.
	 *
	 * @return Indexable[] The created indexables.
	 */
	public function index() {
		$indexables           = [];
		$indexables_to_create = $this->query();

		if ( isset( $indexables_to_create['404'] ) ) {
			$indexables[] = $this->indexable_repository->find_for_system_page( '404' );
		}

		if ( isset( $indexables_to_create['search'] ) ) {
			$indexables[] = $this->indexable_repository->find_for_system_page( 'search-result' );
		}

		if ( isset( $indexables_to_create['date_archive'] ) ) {
			$indexables[] = $this->indexable_repository->find_for_date_archive();
		}
		if ( isset( $indexables_to_create['home_page'] ) ) {
			$indexables[] = $this->indexable_repository->find_for_home_page();
		}

		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );

		return $indexables;
	}

	/**
	 * Returns the number of objects that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		// This matches the maximum number of indexables created by this action.
		return 4;
	}

	/**
	 * Check which indexables already exist and return the values of the ones to create.
	 *
	 * @return array The indexable types to create.
	 */
	private function query() {
		$indexables_to_create = [];
		if ( ! $this->indexable_repository->find_for_system_page( '404', false ) ) {
			$indexables_to_create['404'] = true;
		}

		if ( ! $this->indexable_repository->find_for_system_page( 'search-result', false ) ) {
			$indexables_to_create['search'] = true;
		}

		if ( ! $this->indexable_repository->find_for_date_archive( false ) ) {
			$indexables_to_create['date_archive'] = true;
		}

		$need_home_page_indexable = ( (int) \get_option( 'page_on_front' ) === 0 && \get_option( 'show_on_front' ) === 'posts' );

		if ( $need_home_page_indexable && ! $this->indexable_repository->find_for_home_page( false ) ) {
			$indexables_to_create['home_page'] = true;
		}

		return $indexables_to_create;
	}
}
indexing/indexable-term-indexation-action.php000064400000012163147511254640015411 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Reindexing action for term indexables.
 */
class Indexable_Term_Indexation_Action extends Abstract_Indexing_Action {

	/**
	 * The transient cache key.
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_terms';

	/**
	 * The transient cache key for limited counts.
	 *
	 * @var string
	 */
	public const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';

	/**
	 * The post type helper.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * The latest version of the Indexable term builder
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Indexable_Term_Indexation_Action constructor
	 *
	 * @param Taxonomy_Helper            $taxonomy         The taxonomy helper.
	 * @param Indexable_Repository       $repository       The indexable repository.
	 * @param wpdb                       $wpdb             The WordPress database instance.
	 * @param Indexable_Builder_Versions $builder_versions The latest versions of all indexable builders.
	 */
	public function __construct(
		Taxonomy_Helper $taxonomy,
		Indexable_Repository $repository,
		wpdb $wpdb,
		Indexable_Builder_Versions $builder_versions
	) {
		$this->taxonomy   = $taxonomy;
		$this->repository = $repository;
		$this->wpdb       = $wpdb;
		$this->version    = $builder_versions->get_latest_version_for_type( 'term' );
	}

	/**
	 * Creates indexables for unindexed terms.
	 *
	 * @return Indexable[] The created indexables.
	 */
	public function index() {
		$query = $this->get_select_query( $this->get_limit() );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
		$term_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query );

		$indexables = [];
		foreach ( $term_ids as $term_id ) {
			$indexables[] = $this->repository->find_by_id_and_type( (int) $term_id, 'term' );
		}

		if ( \count( $indexables ) > 0 ) {
			\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
		}

		return $indexables;
	}

	/**
	 * Returns the number of terms that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_term_indexation_limit' - Allow filtering the number of terms indexed during each indexing pass.
		 *
		 * @param int $limit The maximum number of terms indexed.
		 */
		$limit = \apply_filters( 'wpseo_term_indexation_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}

	/**
	 * Builds a query for counting the number of unindexed terms.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_count_query() {
		$indexable_table   = Model::get_table_name( 'Indexable' );
		$taxonomy_table    = $this->wpdb->term_taxonomy;
		$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();

		if ( empty( $public_taxonomies ) ) {
			return '';
		}

		$taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );

		$replacements = [ $this->version ];
		\array_push( $replacements, ...$public_taxonomies );

		// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
		return $this->wpdb->prepare(
			"
			SELECT COUNT(term_id)
			FROM {$taxonomy_table} AS T
			LEFT JOIN $indexable_table AS I
				ON T.term_id = I.object_id
				AND I.object_type = 'term'
				AND I.version = %d
			WHERE I.object_id IS NULL
				AND taxonomy IN ($taxonomies_placeholders)",
			$replacements
		);
	}

	/**
	 * Builds a query for selecting the ID's of unindexed terms.
	 *
	 * @param bool $limit The maximum number of term IDs to return.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_select_query( $limit = false ) {
		$indexable_table   = Model::get_table_name( 'Indexable' );
		$taxonomy_table    = $this->wpdb->term_taxonomy;
		$public_taxonomies = $this->taxonomy->get_indexable_taxonomies();

		if ( empty( $public_taxonomies ) ) {
			return '';
		}

		$placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );

		$replacements = [ $this->version ];
		\array_push( $replacements, ...$public_taxonomies );

		$limit_query = '';
		if ( $limit ) {
			$limit_query    = 'LIMIT %d';
			$replacements[] = $limit;
		}

		// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
		return $this->wpdb->prepare(
			"
			SELECT term_id
			FROM {$taxonomy_table} AS T
			LEFT JOIN $indexable_table AS I
				ON T.term_id = I.object_id
				AND I.object_type = 'term'
				AND I.version = %d
			WHERE I.object_id IS NULL
				AND taxonomy IN ($placeholders)
			$limit_query",
			$replacements
		);
	}
}
indexing/indexation-action-interface.php000064400000001432147511254640014446 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

/**
 * Interface definition of reindexing action for indexables.
 */
interface Indexation_Action_Interface {

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_total_unindexed();

	/**
	 * Indexes a number of objects.
	 *
	 * NOTE: ALWAYS use limits, this method is intended to be called multiple times over several requests.
	 *
	 * For indexing that requires JavaScript simply return the objects that should be indexed.
	 *
	 * @return array The reindexed objects.
	 */
	public function index();

	/**
	 * Returns the number of objects that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit();
}
indexing/term-link-indexing-action.php000064400000006677147511254640014073 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;

/**
 * Reindexing action for term link indexables.
 */
class Term_Link_Indexing_Action extends Abstract_Link_Indexing_Action {

	/**
	 * The transient name.
	 *
	 * @var string
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_unindexed_term_link_count';

	/**
	 * The transient cache key for limited counts.
	 *
	 * @var string
	 */
	public const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';

	/**
	 * The post type helper.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy_helper;

	/**
	 * Sets the required helper.
	 *
	 * @required
	 *
	 * @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
	 *
	 * @return void
	 */
	public function set_helper( Taxonomy_Helper $taxonomy_helper ) {
		$this->taxonomy_helper = $taxonomy_helper;
	}

	/**
	 * Returns objects to be indexed.
	 *
	 * @return array Objects to be indexed.
	 */
	protected function get_objects() {
		$query = $this->get_select_query( $this->get_limit() );

		if ( $query === '' ) {
			return [];
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
		$terms = $this->wpdb->get_results( $query );

		return \array_map(
			static function ( $term ) {
				return (object) [
					'id'      => (int) $term->term_id,
					'type'    => 'term',
					'content' => $term->description,
				];
			},
			$terms
		);
	}

	/**
	 * Builds a query for counting the number of unindexed term links.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_count_query() {
		$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();

		if ( empty( $public_taxonomies ) ) {
			return '';
		}

		$placeholders    = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) );
		$indexable_table = Model::get_table_name( 'Indexable' );

		// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
		return $this->wpdb->prepare(
			"
			SELECT COUNT(T.term_id)
			FROM {$this->wpdb->term_taxonomy} AS T
			LEFT JOIN $indexable_table AS I
				ON T.term_id = I.object_id
				AND I.object_type = 'term'
				AND I.link_count IS NOT NULL
			WHERE I.object_id IS NULL
				AND T.taxonomy IN ($placeholders)",
			$public_taxonomies
		);
	}

	/**
	 * Builds a query for selecting the ID's of unindexed term links.
	 *
	 * @param int|false $limit The maximum number of term link IDs to return.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_select_query( $limit = false ) {
		$public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();

		if ( empty( $public_taxonomies ) ) {
			return '';
		}

		$indexable_table = Model::get_table_name( 'Indexable' );
		$replacements    = $public_taxonomies;

		$limit_query = '';
		if ( $limit ) {
			$limit_query    = 'LIMIT %d';
			$replacements[] = $limit;
		}

		// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
		return $this->wpdb->prepare(
			"
			SELECT T.term_id, T.description
			FROM {$this->wpdb->term_taxonomy} AS T
			LEFT JOIN $indexable_table AS I
				ON T.term_id = I.object_id
				AND I.object_type = 'term'
				AND I.link_count IS NOT NULL
			WHERE I.object_id IS NULL
				AND T.taxonomy IN (" . \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ) . ")
			$limit_query",
			$replacements
		);
	}
}
indexing/indexable-indexing-complete-action.php000064400000001316147511254640015713 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\SEO\Helpers\Indexable_Helper;

/**
 * Indexing action to call when the indexable indexing process is completed.
 */
class Indexable_Indexing_Complete_Action {

	/**
	 * The options helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * Indexable_Indexing_Complete_Action constructor.
	 *
	 * @param Indexable_Helper $indexable_helper The indexable helper.
	 */
	public function __construct( Indexable_Helper $indexable_helper ) {
		$this->indexable_helper = $indexable_helper;
	}

	/**
	 * Wraps up the indexing process.
	 *
	 * @return void
	 */
	public function complete() {
		$this->indexable_helper->finish_indexing();
	}
}
indexing/indexing-complete-action.php000064400000001227147511254640013763 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\SEO\Helpers\Indexing_Helper;

/**
 * Indexing action to call when the indexing is completed.
 */
class Indexing_Complete_Action {

	/**
	 * The indexing helper.
	 *
	 * @var Indexing_Helper
	 */
	protected $indexing_helper;

	/**
	 * Indexing_Complete_Action constructor.
	 *
	 * @param Indexing_Helper $indexing_helper The indexing helper.
	 */
	public function __construct( Indexing_Helper $indexing_helper ) {
		$this->indexing_helper = $indexing_helper;
	}

	/**
	 * Wraps up the indexing process.
	 *
	 * @return void
	 */
	public function complete() {
		$this->indexing_helper->complete();
	}
}
indexing/abstract-link-indexing-action.php000064400000006166147511254640014720 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use wpdb;
use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Models\SEO_Links;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Reindexing action for link indexables.
 */
abstract class Abstract_Link_Indexing_Action extends Abstract_Indexing_Action {

	/**
	 * The link builder.
	 *
	 * @var Indexable_Link_Builder
	 */
	protected $link_builder;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Indexable_Post_Indexing_Action constructor
	 *
	 * @param Indexable_Link_Builder $link_builder     The indexable link builder.
	 * @param Indexable_Helper       $indexable_helper The indexable repository.
	 * @param Indexable_Repository   $repository       The indexable repository.
	 * @param wpdb                   $wpdb             The WordPress database instance.
	 */
	public function __construct(
		Indexable_Link_Builder $link_builder,
		Indexable_Helper $indexable_helper,
		Indexable_Repository $repository,
		wpdb $wpdb
	) {
		$this->link_builder     = $link_builder;
		$this->indexable_helper = $indexable_helper;
		$this->repository       = $repository;
		$this->wpdb             = $wpdb;
	}

	/**
	 * Builds links for indexables which haven't had their links indexed yet.
	 *
	 * @return SEO_Links[] The created SEO links.
	 */
	public function index() {
		$objects = $this->get_objects();

		$indexables = [];
		foreach ( $objects as $object ) {
			$indexable = $this->repository->find_by_id_and_type( $object->id, $object->type );
			if ( $indexable ) {
				$this->link_builder->build( $indexable, $object->content );
				$this->indexable_helper->save_indexable( $indexable );

				$indexables[] = $indexable;
			}
		}

		if ( \count( $indexables ) > 0 ) {
			\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
		}

		return $indexables;
	}

	/**
	 * In the case of term-links and post-links we want to use the total unindexed count, because using
	 * the limited unindexed count actually leads to worse performance.
	 *
	 * @param int|bool $limit Unused.
	 *
	 * @return int The total number of unindexed links.
	 */
	public function get_limited_unindexed_count( $limit = false ) {
		return $this->get_total_unindexed();
	}

	/**
	 * Returns the number of texts that will be indexed in a single link indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_link_indexing_limit' - Allow filtering the number of texts indexed during each link indexing pass.
		 *
		 * @param int $limit The maximum number of texts indexed.
		 */
		return \apply_filters( 'wpseo_link_indexing_limit', 5 );
	}

	/**
	 * Returns objects to be indexed.
	 *
	 * @return array Objects to be indexed, should be an array of objects with object_id, object_type and content.
	 */
	abstract protected function get_objects();
}
indexing/indexable-post-indexation-action.php000064400000013313147511254640015425 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Reindexing action for post indexables.
 */
class Indexable_Post_Indexation_Action extends Abstract_Indexing_Action {

	/**
	 * The transient cache key.
	 *
	 * @var string
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_posts';

	/**
	 * The transient cache key for limited counts.
	 *
	 * @var string
	 */
	public const UNINDEXED_LIMITED_COUNT_TRANSIENT = self::UNINDEXED_COUNT_TRANSIENT . '_limited';

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type_helper;

	/**
	 * The post helper.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * The latest version of Post Indexables.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Indexable_Post_Indexing_Action constructor
	 *
	 * @param Post_Type_Helper           $post_type_helper The post type helper.
	 * @param Indexable_Repository       $repository       The indexable repository.
	 * @param wpdb                       $wpdb             The WordPress database instance.
	 * @param Indexable_Builder_Versions $builder_versions The latest versions for each Indexable type.
	 * @param Post_Helper                $post_helper      The post helper.
	 */
	public function __construct(
		Post_Type_Helper $post_type_helper,
		Indexable_Repository $repository,
		wpdb $wpdb,
		Indexable_Builder_Versions $builder_versions,
		Post_Helper $post_helper
	) {
		$this->post_type_helper = $post_type_helper;
		$this->repository       = $repository;
		$this->wpdb             = $wpdb;
		$this->version          = $builder_versions->get_latest_version_for_type( 'post' );
		$this->post_helper      = $post_helper;
	}

	/**
	 * Creates indexables for unindexed posts.
	 *
	 * @return Indexable[] The created indexables.
	 */
	public function index() {
		$query = $this->get_select_query( $this->get_limit() );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query.
		$post_ids = $this->wpdb->get_col( $query );

		$indexables = [];
		foreach ( $post_ids as $post_id ) {
			$indexables[] = $this->repository->find_by_id_and_type( (int) $post_id, 'post' );
		}

		if ( \count( $indexables ) > 0 ) {
			\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
			\delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT );
		}

		return $indexables;
	}

	/**
	 * Returns the number of posts that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_post_indexation_limit' - Allow filtering the amount of posts indexed during each indexing pass.
		 *
		 * @param int $limit The maximum number of posts indexed.
		 */
		$limit = \apply_filters( 'wpseo_post_indexation_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}

	/**
	 * Builds a query for counting the number of unindexed posts.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_count_query() {
		$indexable_table = Model::get_table_name( 'Indexable' );

		$post_types             = $this->post_type_helper->get_indexable_post_types();
		$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
		$replacements           = \array_merge(
			$post_types,
			$excluded_post_statuses
		);

		$replacements[] = $this->version;

		// Warning: If this query is changed, makes sure to update the query in get_select_query as well.
		// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		return $this->wpdb->prepare(
			"
			SELECT COUNT(P.ID)
			FROM {$this->wpdb->posts} AS P
			WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
			AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
			AND P.ID not in (
				SELECT I.object_id from $indexable_table as I
				WHERE I.object_type = 'post'
				AND I.version = %d )",
			$replacements
		);
	}

	/**
	 * Builds a query for selecting the ID's of unindexed posts.
	 *
	 * @param bool $limit The maximum number of post IDs to return.
	 *
	 * @return string The prepared query string.
	 */
	protected function get_select_query( $limit = false ) {
		$indexable_table = Model::get_table_name( 'Indexable' );

		$post_types             = $this->post_type_helper->get_indexable_post_types();
		$excluded_post_statuses = $this->post_helper->get_excluded_post_statuses();
		$replacements           = \array_merge(
			$post_types,
			$excluded_post_statuses
		);
		$replacements[]         = $this->version;

		$limit_query = '';
		if ( $limit ) {
			$limit_query    = 'LIMIT %d';
			$replacements[] = $limit;
		}

		// Warning: If this query is changed, makes sure to update the query in get_count_query as well.
		// @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		return $this->wpdb->prepare(
			"
			SELECT P.ID
			FROM {$this->wpdb->posts} AS P
			WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ')
			AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ")
			AND P.ID not in (
				SELECT I.object_id from $indexable_table as I
				WHERE I.object_type = 'post'
				AND I.version = %d )
			$limit_query",
			$replacements
		);
	}
}
indexing/limited-indexing-action-interface.php000064400000000763147511254640015544 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

/**
 * Interface definition of a reindexing action for indexables that have a limited unindexed count.
 */
interface Limited_Indexing_Action_Interface {

	/**
	 * Returns a limited number of unindexed posts.
	 *
	 * @param int $limit Limit the maximum number of unindexed posts that are counted.
	 *
	 * @return int|false The limited number of unindexed posts. False if the query fails.
	 */
	public function get_limited_unindexed_count( $limit );
}
indexing/indexable-post-type-archive-indexation-action.php000064400000013737147511254640020035 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexing;

use Yoast\WP\SEO\Builders\Indexable_Builder;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Reindexing action for post type archive indexables.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
 */
class Indexable_Post_Type_Archive_Indexation_Action implements Indexation_Action_Interface, Limited_Indexing_Action_Interface {


	/**
	 * The transient cache key.
	 */
	public const UNINDEXED_COUNT_TRANSIENT = 'wpseo_total_unindexed_post_type_archives';

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $repository;

	/**
	 * The indexable builder.
	 *
	 * @var Indexable_Builder
	 */
	protected $builder;

	/**
	 * The current version of the post type archive indexable builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Indexation_Post_Type_Archive_Action constructor.
	 *
	 * @param Indexable_Repository       $repository The indexable repository.
	 * @param Indexable_Builder          $builder    The indexable builder.
	 * @param Post_Type_Helper           $post_type  The post type helper.
	 * @param Indexable_Builder_Versions $versions   The current versions of all indexable builders.
	 */
	public function __construct(
		Indexable_Repository $repository,
		Indexable_Builder $builder,
		Post_Type_Helper $post_type,
		Indexable_Builder_Versions $versions
	) {
		$this->repository = $repository;
		$this->builder    = $builder;
		$this->post_type  = $post_type;
		$this->version    = $versions->get_latest_version_for_type( 'post-type-archive' );
	}

	/**
	 * Returns the total number of unindexed post type archives.
	 *
	 * @param int $limit Limit the number of counted objects.
	 *
	 * @return int The total number of unindexed post type archives.
	 */
	public function get_total_unindexed( $limit = false ) {
		$transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT );
		if ( $transient !== false ) {
			return (int) $transient;
		}

		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS );

		$result = \count( $this->get_unindexed_post_type_archives( $limit ) );

		\set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS );

		/**
		 * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left.
		 *
		 * @internal
		 */
		\do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result );

		return $result;
	}

	/**
	 * Creates indexables for post type archives.
	 *
	 * @return Indexable[] The created indexables.
	 */
	public function index() {
		$unindexed_post_type_archives = $this->get_unindexed_post_type_archives( $this->get_limit() );

		$indexables = [];
		foreach ( $unindexed_post_type_archives as $post_type_archive ) {
			$indexables[] = $this->builder->build_for_post_type_archive( $post_type_archive );
		}

		if ( \count( $indexables ) > 0 ) {
			\delete_transient( static::UNINDEXED_COUNT_TRANSIENT );
		}

		return $indexables;
	}

	/**
	 * Returns the number of post type archives that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_post_type_archive_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass.
		 *
		 * @param int $limit The maximum number of posts indexed.
		 */
		$limit = \apply_filters( 'wpseo_post_type_archive_indexation_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}

	/**
	 * Retrieves the list of post types for which no indexable for its archive page has been made yet.
	 *
	 * @param int|false $limit Limit the number of retrieved indexables to this number.
	 *
	 * @return array The list of post types for which no indexable for its archive page has been made yet.
	 */
	protected function get_unindexed_post_type_archives( $limit = false ) {
		$post_types_with_archive_pages = $this->get_post_types_with_archive_pages();
		$indexed_post_types            = $this->get_indexed_post_type_archives();

		$unindexed_post_types = \array_diff( $post_types_with_archive_pages, $indexed_post_types );

		if ( $limit ) {
			return \array_slice( $unindexed_post_types, 0, $limit );
		}

		return $unindexed_post_types;
	}

	/**
	 * Returns the names of all the post types that have archive pages.
	 *
	 * @return array The list of names of all post types that have archive pages.
	 */
	protected function get_post_types_with_archive_pages() {
		// We only want to index archive pages of public post types that have them.
		$post_types_with_archive = $this->post_type->get_indexable_post_archives();

		// We only need the post type names, not the objects.
		$post_types = [];
		foreach ( $post_types_with_archive as $post_type_with_archive ) {
			$post_types[] = $post_type_with_archive->name;
		}

		return $post_types;
	}

	/**
	 * Retrieves the list of post type names for which an archive indexable exists.
	 *
	 * @return array The list of names of post types with unindexed archive pages.
	 */
	protected function get_indexed_post_type_archives() {
		$results = $this->repository->query()
			->select( 'object_sub_type' )
			->where( 'object_type', 'post-type-archive' )
			->where_equal( 'version', $this->version )
			->find_array();

		if ( $results === false ) {
			return [];
		}

		$callback = static function ( $result ) {
			return $result['object_sub_type'];
		};

		return \array_map( $callback, $results );
	}

	/**
	 * Returns a limited number of unindexed posts.
	 *
	 * @param int $limit Limit the maximum number of unindexed posts that are counted.
	 *
	 * @return int|false The limited number of unindexed posts. False if the query fails.
	 */
	public function get_limited_unindexed_count( $limit ) {
		return $this->get_total_unindexed( $limit );
	}
}
alert-dismissal-action.php000064400000012576147511254640011651 0ustar00<?php

namespace Yoast\WP\SEO\Actions;

use Yoast\WP\SEO\Helpers\User_Helper;

/**
 * Class Alert_Dismissal_Action.
 */
class Alert_Dismissal_Action {

	public const USER_META_KEY = '_yoast_alerts_dismissed';

	/**
	 * Holds the user helper instance.
	 *
	 * @var User_Helper
	 */
	protected $user;

	/**
	 * Constructs Alert_Dismissal_Action.
	 *
	 * @param User_Helper $user User helper.
	 */
	public function __construct( User_Helper $user ) {
		$this->user = $user;
	}

	/**
	 * Dismisses an alert.
	 *
	 * @param string $alert_identifier Alert identifier.
	 *
	 * @return bool Whether the dismiss was successful or not.
	 */
	public function dismiss( $alert_identifier ) {
		$user_id = $this->user->get_current_user_id();
		if ( $user_id === 0 ) {
			return false;
		}

		if ( $this->is_allowed( $alert_identifier ) === false ) {
			return false;
		}

		$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
		if ( $dismissed_alerts === false ) {
			return false;
		}

		if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === true ) {
			// The alert is already dismissed.
			return true;
		}

		// Add this alert to the dismissed alerts.
		$dismissed_alerts[ $alert_identifier ] = true;

		// Save.
		return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
	}

	/**
	 * Resets an alert.
	 *
	 * @param string $alert_identifier Alert identifier.
	 *
	 * @return bool Whether the reset was successful or not.
	 */
	public function reset( $alert_identifier ) {
		$user_id = $this->user->get_current_user_id();
		if ( $user_id === 0 ) {
			return false;
		}

		if ( $this->is_allowed( $alert_identifier ) === false ) {
			return false;
		}

		$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
		if ( $dismissed_alerts === false ) {
			return false;
		}

		$amount_of_dismissed_alerts = \count( $dismissed_alerts );
		if ( $amount_of_dismissed_alerts === 0 ) {
			// No alerts: nothing to reset.
			return true;
		}

		if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === false ) {
			// Alert not found: nothing to reset.
			return true;
		}

		if ( $amount_of_dismissed_alerts === 1 ) {
			// The 1 remaining dismissed alert is the alert to reset: delete the alerts user meta row.
			return $this->user->delete_meta( $user_id, static::USER_META_KEY, $dismissed_alerts );
		}

		// Remove this alert from the dismissed alerts.
		unset( $dismissed_alerts[ $alert_identifier ] );

		// Save.
		return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false;
	}

	/**
	 * Returns if an alert is dismissed or not.
	 *
	 * @param string $alert_identifier Alert identifier.
	 *
	 * @return bool Whether the alert has been dismissed.
	 */
	public function is_dismissed( $alert_identifier ) {
		$user_id = $this->user->get_current_user_id();
		if ( $user_id === 0 ) {
			return false;
		}

		if ( $this->is_allowed( $alert_identifier ) === false ) {
			return false;
		}

		$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
		if ( $dismissed_alerts === false ) {
			return false;
		}

		return \array_key_exists( $alert_identifier, $dismissed_alerts );
	}

	/**
	 * Returns an object with all alerts dismissed by current user.
	 *
	 * @return array|false An array with the keys of all Alerts that have been dismissed
	 *                     by the current user or `false`.
	 */
	public function all_dismissed() {
		$user_id = $this->user->get_current_user_id();
		if ( $user_id === 0 ) {
			return false;
		}

		$dismissed_alerts = $this->get_dismissed_alerts( $user_id );
		if ( $dismissed_alerts === false ) {
			return false;
		}

		return $dismissed_alerts;
	}

	/**
	 * Returns if an alert is allowed or not.
	 *
	 * @param string $alert_identifier Alert identifier.
	 *
	 * @return bool Whether the alert is allowed.
	 */
	public function is_allowed( $alert_identifier ) {
		return \in_array( $alert_identifier, $this->get_allowed_dismissable_alerts(), true );
	}

	/**
	 * Retrieves the dismissed alerts.
	 *
	 * @param int $user_id User ID.
	 *
	 * @return string[]|false The dismissed alerts. False for an invalid $user_id.
	 */
	protected function get_dismissed_alerts( $user_id ) {
		$dismissed_alerts = $this->user->get_meta( $user_id, static::USER_META_KEY, true );
		if ( $dismissed_alerts === false ) {
			// Invalid user ID.
			return false;
		}

		if ( $dismissed_alerts === '' ) {
			/*
			 * When no database row exists yet, an empty string is returned because of the `single` parameter.
			 * We do want a single result returned, but the default should be an empty array instead.
			 */
			return [];
		}

		return $dismissed_alerts;
	}

	/**
	 * Retrieves the allowed dismissable alerts.
	 *
	 * @return string[] The allowed dismissable alerts.
	 */
	protected function get_allowed_dismissable_alerts() {
		/**
		 * Filter: 'wpseo_allowed_dismissable_alerts' - List of allowed dismissable alerts.
		 *
		 * @param string[] $allowed_dismissable_alerts Allowed dismissable alerts list.
		 */
		$allowed_dismissable_alerts = \apply_filters( 'wpseo_allowed_dismissable_alerts', [] );

		if ( \is_array( $allowed_dismissable_alerts ) === false ) {
			return [];
		}

		// Only allow strings.
		$allowed_dismissable_alerts = \array_filter( $allowed_dismissable_alerts, 'is_string' );

		// Filter unique and reorder indices.
		$allowed_dismissable_alerts = \array_values( \array_unique( $allowed_dismissable_alerts ) );

		return $allowed_dismissable_alerts;
	}
}
importing/importing-indexation-action-interface.php000064400000001445147511254640016663 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Importing;

/**
 * Interface definition of reindexing action for indexables.
 */
interface Importing_Indexation_Action_Interface {

	/**
	 * Returns the total number of unindexed objects.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_total_unindexed();

	/**
	 * Indexes a number of objects.
	 *
	 * NOTE: ALWAYS use limits, this method is intended to be called multiple times over several requests.
	 *
	 * For indexing that requires JavaScript simply return the objects that should be indexed.
	 *
	 * @return array The reindexed objects.
	 */
	public function index();

	/**
	 * Returns the number of objects that will be indexed in a single indexing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit();
}
importing/abstract-aioseo-importing-action.php000064400000015315147511254640015644 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Importing;

use Exception;
use Yoast\WP\SEO\Helpers\Aioseo_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
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;

/**
 * Importing action interface.
 */
abstract class Abstract_Aioseo_Importing_Action implements Importing_Action_Interface {

	/**
	 * The plugin the class deals with.
	 *
	 * @var string
	 */
	public const PLUGIN = null;

	/**
	 * The type the class deals with.
	 *
	 * @var string
	 */
	public const TYPE = null;

	/**
	 * The AIOSEO helper.
	 *
	 * @var Aioseo_Helper
	 */
	protected $aioseo_helper;

	/**
	 * The import cursor helper.
	 *
	 * @var Import_Cursor_Helper
	 */
	protected $import_cursor;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	protected $options;

	/**
	 * The sanitization helper.
	 *
	 * @var Sanitization_Helper
	 */
	protected $sanitization;

	/**
	 * The replacevar handler.
	 *
	 * @var Aioseo_Replacevar_Service
	 */
	protected $replacevar_handler;

	/**
	 * The robots provider service.
	 *
	 * @var Aioseo_Robots_Provider_Service
	 */
	protected $robots_provider;

	/**
	 * The robots transformer service.
	 *
	 * @var Aioseo_Robots_Transformer_Service
	 */
	protected $robots_transformer;

	/**
	 * Abstract_Aioseo_Importing_Action constructor.
	 *
	 * @param Import_Cursor_Helper              $import_cursor      The import cursor helper.
	 * @param Options_Helper                    $options            The options 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.
	 */
	public function __construct(
		Import_Cursor_Helper $import_cursor,
		Options_Helper $options,
		Sanitization_Helper $sanitization,
		Aioseo_Replacevar_Service $replacevar_handler,
		Aioseo_Robots_Provider_Service $robots_provider,
		Aioseo_Robots_Transformer_Service $robots_transformer
	) {
		$this->import_cursor      = $import_cursor;
		$this->options            = $options;
		$this->sanitization       = $sanitization;
		$this->replacevar_handler = $replacevar_handler;
		$this->robots_provider    = $robots_provider;
		$this->robots_transformer = $robots_transformer;
	}

	/**
	 * Sets the AIOSEO helper.
	 *
	 * @required
	 *
	 * @param Aioseo_Helper $aioseo_helper The AIOSEO helper.
	 *
	 * @return void
	 */
	public function set_aioseo_helper( Aioseo_Helper $aioseo_helper ) {
		$this->aioseo_helper = $aioseo_helper;
	}

	/**
	 * The name of the plugin we import from.
	 *
	 * @return string The plugin we import from.
	 *
	 * @throws Exception If the PLUGIN constant is not set in the child class.
	 */
	public function get_plugin() {
		$class  = static::class;
		$plugin = $class::PLUGIN;

		if ( $plugin === null ) {
			throw new Exception( 'Importing action without explicit plugin' );
		}

		return $plugin;
	}

	/**
	 * The data type we import from the plugin.
	 *
	 * @return string The data type we import from the plugin.
	 *
	 * @throws Exception If the TYPE constant is not set in the child class.
	 */
	public function get_type() {
		$class = static::class;
		$type  = $class::TYPE;

		if ( $type === null ) {
			throw new Exception( 'Importing action without explicit type' );
		}

		return $type;
	}

	/**
	 * Can the current action import the data from plugin $plugin of type $type?
	 *
	 * @param string|null $plugin The plugin to import from.
	 * @param string|null $type   The type of data to import.
	 *
	 * @return bool True if this action can handle the combination of Plugin and Type.
	 *
	 * @throws Exception If the TYPE constant is not set in the child class.
	 */
	public function is_compatible_with( $plugin = null, $type = null ) {
		if ( empty( $plugin ) && empty( $type ) ) {
			return true;
		}

		if ( $plugin === $this->get_plugin() && empty( $type ) ) {
			return true;
		}

		if ( empty( $plugin ) && $type === $this->get_type() ) {
			return true;
		}

		if ( $plugin === $this->get_plugin() && $type === $this->get_type() ) {
			return true;
		}

		return false;
	}

	/**
	 * Gets the completed id (to be used as a key for the importing_completed option).
	 *
	 * @return string The completed id.
	 */
	public function get_completed_id() {
		return $this->get_cursor_id();
	}

	/**
	 * Returns the stored state of completedness.
	 *
	 * @return int The stored state of completedness.
	 */
	public function get_completed() {
		$completed_id          = $this->get_completed_id();
		$importers_completions = $this->options->get( 'importing_completed', [] );

		return ( isset( $importers_completions[ $completed_id ] ) ) ? $importers_completions[ $completed_id ] : false;
	}

	/**
	 * Stores the current state of completedness.
	 *
	 * @param bool $completed Whether the importer is completed.
	 *
	 * @return void
	 */
	public function set_completed( $completed ) {
		$completed_id                  = $this->get_completed_id();
		$current_importers_completions = $this->options->get( 'importing_completed', [] );

		$current_importers_completions[ $completed_id ] = $completed;
		$this->options->set( 'importing_completed', $current_importers_completions );
	}

	/**
	 * Returns whether the importing action is enabled.
	 *
	 * @return bool True by default unless a child class overrides it.
	 */
	public function is_enabled() {
		return true;
	}

	/**
	 * Gets the cursor id.
	 *
	 * @return string The cursor id.
	 */
	protected function get_cursor_id() {
		return $this->get_plugin() . '_' . $this->get_type();
	}

	/**
	 * Minimally transforms data to be imported.
	 *
	 * @param string $meta_data The meta data to be imported.
	 *
	 * @return string The transformed meta data.
	 */
	public function simple_import( $meta_data ) {
		// Transform the replace vars into Yoast replace vars.
		$transformed_data = $this->replacevar_handler->transform( $meta_data );

		return $this->sanitization->sanitize_text_field( \html_entity_decode( $transformed_data ) );
	}

	/**
	 * Transforms URL to be imported.
	 *
	 * @param string $meta_data The meta data to be imported.
	 *
	 * @return string The transformed URL.
	 */
	public function url_import( $meta_data ) {
		// We put null as the allowed protocols here, to have the WP default allowed protocols, see https://developer.wordpress.org/reference/functions/wp_allowed_protocols.
		return $this->sanitization->sanitize_url( $meta_data, null );
	}
}
importing/deactivate-conflicting-plugins-action.php000064400000010417147511254640016641 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Importing;

use Yoast\WP\SEO\Conditionals\Updated_Importer_Framework_Conditional;
use Yoast\WP\SEO\Config\Conflicting_Plugins;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
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\Conflicting_Plugins_Service;

/**
 * Deactivates plug-ins that cause conflicts with Yoast SEO.
 */
class Deactivate_Conflicting_Plugins_Action extends Abstract_Aioseo_Importing_Action {

	/**
	 * The plugin the class deals with.
	 *
	 * @var string
	 */
	public const PLUGIN = 'conflicting-plugins';

	/**
	 * The type the class deals with.
	 *
	 * @var string
	 */
	public const TYPE = 'deactivation';

	/**
	 * The replacevar handler.
	 *
	 * @var Aioseo_Replacevar_Service
	 */
	protected $replacevar_handler;

	/**
	 * Knows all plugins that might possibly conflict.
	 *
	 * @var Conflicting_Plugins_Service
	 */
	protected $conflicting_plugins;

	/**
	 * The list of conflicting plugins
	 *
	 * @var array
	 */
	protected $detected_plugins;

	/**
	 * Class constructor.
	 *
	 * @param Import_Cursor_Helper              $import_cursor               The import cursor helper.
	 * @param Options_Helper                    $options                     The options 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 Conflicting_Plugins_Service       $conflicting_plugins_service The Conflicting plugins Service.
	 */
	public function __construct(
		Import_Cursor_Helper $import_cursor,
		Options_Helper $options,
		Sanitization_Helper $sanitization,
		Aioseo_Replacevar_Service $replacevar_handler,
		Aioseo_Robots_Provider_Service $robots_provider,
		Aioseo_Robots_Transformer_Service $robots_transformer,
		Conflicting_Plugins_Service $conflicting_plugins_service
	) {
		parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );

		$this->conflicting_plugins = $conflicting_plugins_service;
		$this->detected_plugins    = [];
	}

	/**
	 * Get the total number of conflicting plugins.
	 *
	 * @return int
	 */
	public function get_total_unindexed() {
		return \count( $this->get_detected_plugins() );
	}

	/**
	 * Returns whether the updated importer framework is enabled.
	 *
	 * @return bool True if the updated importer framework is enabled.
	 */
	public function is_enabled() {
		$updated_importer_framework_conditional = \YoastSEO()->classes->get( Updated_Importer_Framework_Conditional::class );

		return $updated_importer_framework_conditional->is_met();
	}

	/**
	 * Deactivate conflicting plugins.
	 *
	 * @return array
	 */
	public function index() {
		$detected_plugins = $this->get_detected_plugins();
		$this->conflicting_plugins->deactivate_conflicting_plugins( $detected_plugins );

		// We need to conform to the interface, so we report that no indexables were created.
		return [];
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_limit() {
		return \count( Conflicting_Plugins::all_plugins() );
	}

	/**
	 * Returns the total number of unindexed objects up to a limit.
	 *
	 * @param int $limit The maximum.
	 *
	 * @return int The total number of unindexed objects.
	 */
	public function get_limited_unindexed_count( $limit ) {
		$count = \count( $this->get_detected_plugins() );
		return ( $count <= $limit ) ? $count : $limit;
	}

	/**
	 * Returns all detected plugins.
	 *
	 * @return array The detected plugins.
	 */
	protected function get_detected_plugins() {
		// The active plugins won't change much. We can reuse the result for the duration of the request.
		if ( \count( $this->detected_plugins ) < 1 ) {
			$this->detected_plugins = $this->conflicting_plugins->detect_conflicting_plugins();
		}
		return $this->detected_plugins;
	}
}
importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php000064400000005540147511254640022651 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

/**
 * Importing action for AIOSEO posttype defaults settings data.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
 */
class Aioseo_Posttype_Defaults_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'posttype_default_settings';

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with.
	 *
	 * @var string
	 */
	protected $settings_tab = 'postTypes';

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	protected function build_mapping() {
		$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );

		foreach ( $post_type_objects as $pt ) {
			// Use all the custom post types that are public.
			$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ]                       = [
				'yoast_name'       => 'title-' . $pt->name,
				'transform_method' => 'simple_import',
			];
			$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ]             = [
				'yoast_name'       => 'metadesc-' . $pt->name,
				'transform_method' => 'simple_import',
			];
			$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/showMetaBox' ]        = [
				'yoast_name'       => 'display-metabox-pt-' . $pt->name,
				'transform_method' => 'simple_boolean_import',
			];
			$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
				'yoast_name'       => 'noindex-' . $pt->name,
				'transform_method' => 'import_noindex',
				'type'             => 'postTypes',
				'subtype'          => $pt->name,
				'option_name'      => 'aioseo_options_dynamic',
			];

			if ( $pt->name === 'attachment' ) {
				$this->aioseo_options_to_yoast_map['/attachment/redirectAttachmentUrls'] = [
					'yoast_name'       => 'disable-attachment',
					'transform_method' => 'import_redirect_attachment',
				];
			}
		}
	}

	/**
	 * Transforms the redirect_attachment setting.
	 *
	 * @param string $redirect_attachment The redirect_attachment setting.
	 *
	 * @return bool The transformed redirect_attachment setting.
	 */
	public function import_redirect_attachment( $redirect_attachment ) {
		switch ( $redirect_attachment ) {
			case 'disabled':
				return false;

			case 'attachment':
			case 'attachment_parent':
			default:
				return true;
		}
	}
}
importing/aioseo/aioseo-taxonomy-settings-importing-action.php000064400000007645147511254640021043 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

/**
 * Importing action for AIOSEO taxonomies settings data.
 */
class Aioseo_Taxonomy_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'taxonomy_settings';

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with.
	 *
	 * @var string
	 */
	protected $settings_tab = 'taxonomies';

	/**
	 * Additional mapping between AiOSEO replace vars and Yoast replace vars.
	 *
	 * @var array
	 *
	 * @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
	 */
	protected $replace_vars_edited_map = [
		'#breadcrumb_404_error_format'         => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_archive_post_type_format' => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_archive_post_type_name'   => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_author_display_name'      => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_author_first_name'        => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_blog_page_title'          => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_label'                    => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_link'                     => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_search_result_format'     => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_search_string'            => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_separator'                => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#breadcrumb_taxonomy_title'           => '', // Empty string, as AIOSEO shows nothing for that tag.
		'#taxonomy_title'                      => '%%term_title%%',
	];

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	protected function build_mapping() {
		$taxonomy_objects = \get_taxonomies( [ 'public' => true ], 'object' );

		foreach ( $taxonomy_objects as $tax ) {
			// Use all the public taxonomies.
			$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/title' ]                       = [
				'yoast_name'       => 'title-tax-' . $tax->name,
				'transform_method' => 'simple_import',
			];
			$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/metaDescription' ]             = [
				'yoast_name'       => 'metadesc-tax-' . $tax->name,
				'transform_method' => 'simple_import',
			];
			$this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/advanced/robotsMeta/noindex' ] = [
				'yoast_name'       => 'noindex-tax-' . $tax->name,
				'transform_method' => 'import_noindex',
				'type'             => 'taxonomies',
				'subtype'          => $tax->name,
				'option_name'      => 'aioseo_options_dynamic',
			];
		}
	}

	/**
	 * Returns a setting map of the robot setting for post category taxonomies.
	 *
	 * @return array The setting map of the robot setting for post category taxonomies.
	 */
	public function pluck_robot_setting_from_mapping() {
		$this->build_mapping();

		foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
			// Return the first archive setting map.
			if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'category' ) {
				return $setting;
			}
		}

		return [];
	}
}
importing/aioseo/aioseo-posts-importing-action.php000064400000054742147511254640016477 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use wpdb;
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.
	 *
	 * @var array
	 */
	protected $aioseo_to_yoast_map = [
		'title'               => [
			'yoast_name'       => 'title',
			'transform_method' => 'simple_import_post',
		],
		'description'         => [
			'yoast_name'       => 'description',
			'transform_method' => 'simple_import_post',
		],
		'og_title'            => [
			'yoast_name'       => 'open_graph_title',
			'transform_method' => 'simple_import_post',
		],
		'og_description'      => [
			'yoast_name'       => 'open_graph_description',
			'transform_method' => 'simple_import_post',
		],
		'twitter_title'       => [
			'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,
		],
		'canonical_url'       => [
			'yoast_name'       => 'canonical',
			'transform_method' => 'url_import_post',
		],
		'keyphrases'          => [
			'yoast_name'       => 'primary_focus_keyword',
			'transform_method' => 'keyphrase_import',
		],
		'og_image_url'        => [
			'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',
		],
		'twitter_image_url'   => [
			'yoast_name'                   => 'twitter_image',
			'social_image_import'          => true,
			'social_setting_prefix_aioseo' => 'twitter_',
			'social_setting_prefix_yoast'  => 'twitter_',
			'transform_method'             => 'social_image_url_import',
		],
		'robots_noindex'      => [
			'yoast_name'       => 'is_robots_noindex',
			'transform_method' => 'post_robots_noindex_import',
			'robots_import'    => true,
		],
		'robots_nofollow'     => [
			'yoast_name'       => 'is_robots_nofollow',
			'transform_method' => 'post_general_robots_import',
			'robots_import'    => true,
			'robot_type'       => 'nofollow',
		],
		'robots_noarchive'    => [
			'yoast_name'       => 'is_robots_noarchive',
			'transform_method' => 'post_general_robots_import',
			'robots_import'    => true,
			'robot_type'       => 'noarchive',
		],
		'robots_nosnippet'    => [
			'yoast_name'       => 'is_robots_nosnippet',
			'transform_method' => 'post_general_robots_import',
			'robots_import'    => true,
			'robot_type'       => 'nosnippet',
		],
		'robots_noimageindex' => [
			'yoast_name'       => 'is_robots_noimageindex',
			'transform_method' => 'post_general_robots_import',
			'robots_import'    => true,
			'robot_type'       => 'noimageindex',
		],
	];

	/**
	 * Represents the indexables repository.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * The image helper.
	 *
	 * @var Image_Helper
	 */
	protected $image;

	/**
	 * The indexable_to_postmeta helper.
	 *
	 * @var Indexable_To_Postmeta_Helper
	 */
	protected $indexable_to_postmeta;

	/**
	 * The indexable helper.
	 *
	 * @var Indexable_Helper
	 */
	protected $indexable_helper;

	/**
	 * The social images provider service.
	 *
	 * @var Aioseo_Social_Images_Provider_Service
	 */
	protected $social_images_provider;

	/**
	 * Class constructor.
	 *
	 * @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,
		wpdb $wpdb,
		Import_Cursor_Helper $import_cursor,
		Indexable_Helper $indexable_helper,
		Indexable_To_Postmeta_Helper $indexable_to_postmeta,
		Options_Helper $options,
		Image_Helper $image,
		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->wpdb                   = $wpdb;
		$this->image                  = $image;
		$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() ) {
			return 0;
		}

		$limit                = false;
		$just_detect          = true;
		$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() ) {
			return 0;
		}

		$just_detect          = true;
		$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() ) {
			return false;
		}

		$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' ) ) {
				continue;
			}

			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 ) {
			// For robots import.
			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 );

				continue;
			}

			// 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;
					}
				}
				continue;
			}

			// 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 );
			}
		}

		return $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.
	 *
	 * @return int The limit.
	 */
	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 ) {
			$limit = 25;
		}

		return $limit;
	}

	/**
	 * 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' );

		return $needed_data;
	}

	/**
	 * 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() {
		$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 ( ! $just_detect ) {
			// 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 ];

		$limit_statement = '';
		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}",
			$replacements
		);
		// 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 ) {
		$meta_data = \json_decode( $aioseo_data[ $aioseo_key ], true );
		if ( ! isset( $meta_data['focus']['keyphrase'] ) ) {
			return null;
		}

		return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] );
	}

	/**
	 * Imports the post's noindex setting.
	 *
	 * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post.
	 *
	 * @return bool|null The value of Yoast's noindex setting for the post.
	 */
	public function post_robots_noindex_import( $aioseo_robots_settings ) {
		// If robot settings defer to default settings, we have null in the is_robots_noindex field.
		if ( $aioseo_robots_settings['robots_default'] ) {
			return null;
		}

		return $aioseo_robots_settings['robots_noindex'];
	}

	/**
	 * Imports the post's robots setting.
	 *
	 * @param bool   $aioseo_robots_settings AIOSEO's set of robot settings for the post.
	 * @param string $aioseo_key             The AIOSEO key that contains the robot setting we're working with.
	 * @param array  $mapping                The mapping of the setting we're working with.
	 *
	 * @return bool|null The value of Yoast's noindex setting for the post.
	 */
	public function post_general_robots_import( $aioseo_robots_settings, $aioseo_key, $mapping ) {
		$mapping = $this->enhance_mapping( $mapping );

		if ( $aioseo_robots_settings['robots_default'] ) {
			// Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults.
			$subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping );
			return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping );
		}

		return $aioseo_robots_settings[ $aioseo_key ];
	}

	/**
	 * Enhances the mapping of the setting we're working with, with type and the option name, so that we can retrieve the settings for the object we're working with.
	 *
	 * @param array $mapping The mapping of the setting we're working with.
	 *
	 * @return array The enhanced mapping.
	 */
	public function enhance_mapping( $mapping = [] ) {
		$mapping['type']        = 'postTypes';
		$mapping['option_name'] = 'aioseo_options_dynamic';

		return $mapping;
	}

	/**
	 * Imports the og and twitter image url.
	 *
	 * @param bool      $aioseo_social_image_settings AIOSEO's set of social image settings for the post.
	 * @param string    $aioseo_key                   The AIOSEO key that contains the robot setting we're working with.
	 * @param array     $mapping                      The mapping of the setting we're working with.
	 * @param Indexable $indexable                    The Yoast indexable we're importing into.
	 *
	 * @return bool|null The url of the social image we're importing, null if there's none.
	 */
	public function social_image_url_import( $aioseo_social_image_settings, $aioseo_key, $mapping, $indexable ) {
		if ( $mapping['social_setting_prefix_aioseo'] === 'twitter_' && $aioseo_social_image_settings['twitter_use_og'] ) {
			$mapping['social_setting_prefix_aioseo'] = 'og_';
		}

		$social_setting = \rtrim( $mapping['social_setting_prefix_aioseo'], '_' );

		$image_type = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_type' ];

		if ( $image_type === 'default' ) {
			$image_type = $this->social_images_provider->get_default_social_image_source( $social_setting );
		}

		switch ( $image_type ) {
			case 'attach':
				$image_url = $this->social_images_provider->get_first_attached_image( $indexable->object_id );
				break;
			case 'auto':
				if ( $this->social_images_provider->get_featured_image( $indexable->object_id ) ) {
					// If there's a featured image, lets not import it, as our indexable calculation has already set that as active social image. That way we achieve dynamicality.
					return null;
				}
				$image_url = $this->social_images_provider->get_auto_image( $indexable->object_id );
				break;
			case 'content':
				$image_url = $this->social_images_provider->get_first_image_in_content( $indexable->object_id );
				break;
			case 'custom_image':
				$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_custom_url' ];
				break;
			case 'featured':
				return null; // Our auto-calculation when the indexable was built/updated has taken care of it, so it's not needed to transfer any data now.
			case 'author':
				return null;
			case 'custom':
				return null;
			case 'default':
				$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
				break;
			default:
				$image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_url' ];
				break;
		}

		if ( empty( $image_url ) ) {
			$image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting );
		}

		if ( empty( $image_url ) ) {
			return null;
		}

		return $this->sanitization->sanitize_url( $image_url, null );
	}
}
importing/aioseo/aioseo-default-archive-settings-importing-action.php000064400000005610147511254640022216 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

/**
 * Importing action for AIOSEO default archive settings data.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
 */
class Aioseo_Default_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'default_archive_settings';

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = 'aioseo_options';

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with.
	 *
	 * @var string
	 */
	protected $settings_tab = 'archives';

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	protected function build_mapping() {
		$this->aioseo_options_to_yoast_map = [
			'/author/title'                       => [
				'yoast_name'       => 'title-author-wpseo',
				'transform_method' => 'simple_import',
			],
			'/author/metaDescription'             => [
				'yoast_name'       => 'metadesc-author-wpseo',
				'transform_method' => 'simple_import',
			],
			'/date/title'                         => [
				'yoast_name'       => 'title-archive-wpseo',
				'transform_method' => 'simple_import',
			],
			'/date/metaDescription'               => [
				'yoast_name'       => 'metadesc-archive-wpseo',
				'transform_method' => 'simple_import',
			],
			'/search/title'                       => [
				'yoast_name'       => 'title-search-wpseo',
				'transform_method' => 'simple_import',
			],
			'/author/advanced/robotsMeta/noindex' => [
				'yoast_name'       => 'noindex-author-wpseo',
				'transform_method' => 'import_noindex',
				'type'             => 'archives',
				'subtype'          => 'author',
				'option_name'      => 'aioseo_options',
			],
			'/date/advanced/robotsMeta/noindex'   => [
				'yoast_name'       => 'noindex-archive-wpseo',
				'transform_method' => 'import_noindex',
				'type'             => 'archives',
				'subtype'          => 'date',
				'option_name'      => 'aioseo_options',
			],
		];
	}

	/**
	 * Returns a setting map of the robot setting for author archives.
	 *
	 * @return array The setting map of the robot setting for author archives.
	 */
	public function pluck_robot_setting_from_mapping() {
		$this->build_mapping();

		foreach ( $this->aioseo_options_to_yoast_map as $setting ) {
			// Return the first archive setting map.
			if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'author' ) {
				return $setting;
			}
		}

		return [];
	}
}
importing/aioseo/aioseo-cleanup-action.php000064400000011022147511254640014730 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use wpdb;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Importing action for cleaning up AIOSEO data.
 */
class Aioseo_Cleanup_Action extends Abstract_Aioseo_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'cleanup';

	/**
	 * The AIOSEO meta_keys to be cleaned up.
	 *
	 * @var array
	 */
	protected $aioseo_postmeta_keys = [
		'_aioseo_title',
		'_aioseo_description',
		'_aioseo_og_title',
		'_aioseo_og_description',
		'_aioseo_twitter_title',
		'_aioseo_twitter_description',
	];

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Class constructor.
	 *
	 * @param wpdb           $wpdb    The WordPress database instance.
	 * @param Options_Helper $options The options helper.
	 */
	public function __construct(
		wpdb $wpdb,
		Options_Helper $options
	) {
		$this->wpdb    = $wpdb;
		$this->options = $options;
	}

	/**
	 * Retrieves the postmeta along with the db prefix.
	 *
	 * @return string The postmeta table name along with the db prefix.
	 */
	protected function get_postmeta_table() {
		return $this->wpdb->prefix . 'postmeta';
	}

	/**
	 * Just checks if the cleanup has been completed in the past.
	 *
	 * @return int The total number of unimported objects.
	 */
	public function get_total_unindexed() {
		if ( ! $this->aioseo_helper->aioseo_exists() ) {
			return 0;
		}

		return ( ! $this->get_completed() ) ? 1 : 0;
	}

	/**
	 * Just checks if the cleanup has been completed in the past.
	 *
	 * @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() ) {
			return 0;
		}

		return ( ! $this->get_completed() ) ? 1 : 0;
	}

	/**
	 * Cleans up AIOSEO data.
	 *
	 * @return Indexable[]|false An array of created indexables or false if aioseo data was not found.
	 */
	public function index() {
		if ( $this->get_completed() ) {
			return [];
		}

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no unescaped user input.
		$meta_data                  = $this->wpdb->query( $this->cleanup_postmeta_query() );
		$aioseo_table_truncate_done = $this->wpdb->query( $this->truncate_query() );
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared

		if ( $meta_data === false && $aioseo_table_truncate_done === false ) {
			return false;
		}

		$this->set_completed( true );

		return [
			'metadata_cleanup'   => $meta_data,
			'indexables_cleanup' => $aioseo_table_truncate_done,
		];
	}

	/**
	 * Creates a DELETE query string for deleting AIOSEO postmeta data.
	 *
	 * @return string The query to use for importing or counting the number of items to import.
	 */
	public function cleanup_postmeta_query() {
		$table               = $this->get_postmeta_table();
		$meta_keys_to_delete = $this->aioseo_postmeta_keys;

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
		return $this->wpdb->prepare(
			"DELETE FROM {$table} WHERE meta_key IN (" . \implode( ', ', \array_fill( 0, \count( $meta_keys_to_delete ), '%s' ) ) . ')',
			$meta_keys_to_delete
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Creates a TRUNCATE query string for emptying the AIOSEO indexable table, if it exists.
	 *
	 * @return string The query to use for importing or counting the number of items to import.
	 */
	public function truncate_query() {
		if ( ! $this->aioseo_helper->aioseo_exists() ) {
			// If the table doesn't exist, we need a string that will amount to a quick query that doesn't return false when ran.
			return 'SELECT 1';
		}

		$table = $this->aioseo_helper->get_table();

		return "TRUNCATE TABLE {$table}";
	}

	/**
	 * Used nowhere. Exists to comply with the interface.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of posts indexed during each indexing pass.
		 *
		 * @param int $max_posts The maximum number of posts cleaned up.
		 */
		$limit = \apply_filters( 'wpseo_aioseo_cleanup_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}
}
importing/aioseo/aioseo-custom-archive-settings-importing-action.php000064400000007366147511254640022116 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
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;

/**
 * Importing action for AIOSEO custom archive settings data.
 *
 * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
 */
class Aioseo_Custom_Archive_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'custom_archive_settings';

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = 'aioseo_options_dynamic';

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with.
	 *
	 * @var string
	 */
	protected $settings_tab = 'archives';

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	protected $post_type;

	/**
	 * Aioseo_Custom_Archive_Settings_Importing_Action constructor.
	 *
	 * @param Import_Cursor_Helper              $import_cursor      The import cursor helper.
	 * @param Options_Helper                    $options            The options helper.
	 * @param Sanitization_Helper               $sanitization       The sanitization helper.
	 * @param Post_Type_Helper                  $post_type          The post type 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.
	 */
	public function __construct(
		Import_Cursor_Helper $import_cursor,
		Options_Helper $options,
		Sanitization_Helper $sanitization,
		Post_Type_Helper $post_type,
		Aioseo_Replacevar_Service $replacevar_handler,
		Aioseo_Robots_Provider_Service $robots_provider,
		Aioseo_Robots_Transformer_Service $robots_transformer
	) {
		parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );

		$this->post_type = $post_type;
	}

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	protected function build_mapping() {
		$post_type_objects = \get_post_types( [ 'public' => true ], 'objects' );

		foreach ( $post_type_objects as $pt ) {
			// Use all the custom post types that have archives.
			if ( ! $pt->_builtin && $this->post_type->has_archive( $pt ) ) {
				$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ]                       = [
					'yoast_name'       => 'title-ptarchive-' . $pt->name,
					'transform_method' => 'simple_import',
				];
				$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ]             = [
					'yoast_name'       => 'metadesc-ptarchive-' . $pt->name,
					'transform_method' => 'simple_import',
				];
				$this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [
					'yoast_name'       => 'noindex-ptarchive-' . $pt->name,
					'transform_method' => 'import_noindex',
					'type'             => 'archives',
					'subtype'          => $pt->name,
					'option_name'      => 'aioseo_options_dynamic',
				];
			}
		}
	}
}
importing/aioseo/aioseo-general-settings-importing-action.php000064400000013752147511254640020576 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Import_Cursor_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Sanitization_Helper;
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;

/**
 * Importing action for AIOSEO general settings.
 */
class Aioseo_General_Settings_Importing_Action extends Abstract_Aioseo_Settings_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'general_settings';

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = 'aioseo_options';

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with.
	 *
	 * @var string
	 */
	protected $settings_tab = 'global';

	/**
	 * The image helper.
	 *
	 * @var Image_Helper
	 */
	protected $image;

	/**
	 * Aioseo_General_Settings_Importing_Action constructor.
	 *
	 * @param Import_Cursor_Helper              $import_cursor      The import cursor helper.
	 * @param Options_Helper                    $options            The options helper.
	 * @param Sanitization_Helper               $sanitization       The sanitization helper.
	 * @param Image_Helper                      $image              The image 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.
	 */
	public function __construct(
		Import_Cursor_Helper $import_cursor,
		Options_Helper $options,
		Sanitization_Helper $sanitization,
		Image_Helper $image,
		Aioseo_Replacevar_Service $replacevar_handler,
		Aioseo_Robots_Provider_Service $robots_provider,
		Aioseo_Robots_Transformer_Service $robots_transformer
	) {
		parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer );

		$this->image = $image;
	}

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	protected function build_mapping() {
		$this->aioseo_options_to_yoast_map = [
			'/separator'               => [
				'yoast_name'       => 'separator',
				'transform_method' => 'transform_separator',
			],
			'/siteTitle'               => [
				'yoast_name'       => 'title-home-wpseo',
				'transform_method' => 'simple_import',
			],
			'/metaDescription'         => [
				'yoast_name'       => 'metadesc-home-wpseo',
				'transform_method' => 'simple_import',
			],
			'/schema/siteRepresents'   => [
				'yoast_name'       => 'company_or_person',
				'transform_method' => 'transform_site_represents',
			],
			'/schema/person'           => [
				'yoast_name'       => 'company_or_person_user_id',
				'transform_method' => 'simple_import',
			],
			'/schema/organizationName' => [
				'yoast_name'       => 'company_name',
				'transform_method' => 'simple_import',
			],
			'/schema/organizationLogo' => [
				'yoast_name'       => 'company_logo',
				'transform_method' => 'import_company_logo',
			],
			'/schema/personLogo'       => [
				'yoast_name'       => 'person_logo',
				'transform_method' => 'import_person_logo',
			],
		];
	}

	/**
	 * Imports the organization logo while also accounting for the id of the log to be saved in the separate Yoast option.
	 *
	 * @param string $logo_url The company logo url coming from AIOSEO settings.
	 *
	 * @return string The transformed company logo url.
	 */
	public function import_company_logo( $logo_url ) {
		$logo_id = $this->image->get_attachment_by_url( $logo_url );
		$this->options->set( 'company_logo_id', $logo_id );

		$this->options->set( 'company_logo_meta', false );
		$logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' );
		$this->options->set( 'company_logo_meta', $logo_meta );

		return $this->url_import( $logo_url );
	}

	/**
	 * Imports the person logo while also accounting for the id of the log to be saved in the separate Yoast option.
	 *
	 * @param string $logo_url The person logo url coming from AIOSEO settings.
	 *
	 * @return string The transformed person logo url.
	 */
	public function import_person_logo( $logo_url ) {
		$logo_id = $this->image->get_attachment_by_url( $logo_url );
		$this->options->set( 'person_logo_id', $logo_id );

		$this->options->set( 'person_logo_meta', false );
		$logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' );
		$this->options->set( 'person_logo_meta', $logo_meta );

		return $this->url_import( $logo_url );
	}

	/**
	 * Transforms the site represents setting.
	 *
	 * @param string $site_represents The site represents setting.
	 *
	 * @return string The transformed site represents setting.
	 */
	public function transform_site_represents( $site_represents ) {
		switch ( $site_represents ) {
			case 'person':
				return 'person';

			case 'organization':
			default:
				return 'company';
		}
	}

	/**
	 * Transforms the separator setting.
	 *
	 * @param string $separator The separator setting.
	 *
	 * @return string The transformed separator.
	 */
	public function transform_separator( $separator ) {
		switch ( $separator ) {
			case '&#45;':
				return 'sc-dash';

			case '&ndash;':
				return 'sc-ndash';

			case '&mdash;':
				return 'sc-mdash';

			case '&raquo;':
				return 'sc-raquo';

			case '&laquo;':
				return 'sc-laquo';

			case '&gt;':
				return 'sc-gt';

			case '&bull;':
				return 'sc-bull';

			case '&#124;':
				return 'sc-pipe';

			default:
				return 'sc-dash';
		}
	}
}
importing/aioseo/aioseo-validate-data-action.php000064400000021157147511254640016013 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use wpdb;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Exceptions\Importing\Aioseo_Validation_Exception;
use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Importing action for validating AIOSEO data before the import occurs.
 */
class Aioseo_Validate_Data_Action extends Abstract_Aioseo_Importing_Action {

	/**
	 * The plugin of the action.
	 */
	public const PLUGIN = 'aioseo';

	/**
	 * The type of the action.
	 */
	public const TYPE = 'validate_data';

	/**
	 * The WordPress database instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * The Post Importing action.
	 *
	 * @var Aioseo_Posts_Importing_Action
	 */
	protected $post_importing_action;

	/**
	 * The settings importing actions.
	 *
	 * @var array
	 */
	protected $settings_importing_actions;

	/**
	 * Class constructor.
	 *
	 * @param wpdb                                               $wpdb                              The WordPress database instance.
	 * @param Options_Helper                                     $options                           The options helper.
	 * @param Aioseo_Custom_Archive_Settings_Importing_Action    $custom_archive_action             The Custom Archive Settings importing action.
	 * @param Aioseo_Default_Archive_Settings_Importing_Action   $default_archive_action            The Default Archive Settings importing action.
	 * @param Aioseo_General_Settings_Importing_Action           $general_settings_action           The General Settings importing action.
	 * @param Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action The Posttype Defaults Settings importing action.
	 * @param Aioseo_Taxonomy_Settings_Importing_Action          $taxonomy_settings_action          The Taxonomy Settings importing action.
	 * @param Aioseo_Posts_Importing_Action                      $post_importing_action             The Post importing action.
	 */
	public function __construct(
		wpdb $wpdb,
		Options_Helper $options,
		Aioseo_Custom_Archive_Settings_Importing_Action $custom_archive_action,
		Aioseo_Default_Archive_Settings_Importing_Action $default_archive_action,
		Aioseo_General_Settings_Importing_Action $general_settings_action,
		Aioseo_Posttype_Defaults_Settings_Importing_Action $posttype_defaults_settings_action,
		Aioseo_Taxonomy_Settings_Importing_Action $taxonomy_settings_action,
		Aioseo_Posts_Importing_Action $post_importing_action
	) {
		$this->wpdb                       = $wpdb;
		$this->options                    = $options;
		$this->post_importing_action      = $post_importing_action;
		$this->settings_importing_actions = [
			$custom_archive_action,
			$default_archive_action,
			$general_settings_action,
			$posttype_defaults_settings_action,
			$taxonomy_settings_action,
		];
	}

	/**
	 * Just checks if the action has been completed in the past.
	 *
	 * @return int 1 if it hasn't been completed in the past, 0 if it has.
	 */
	public function get_total_unindexed() {
		return ( ! $this->get_completed() ) ? 1 : 0;
	}

	/**
	 * Just checks if the action has been completed in the past.
	 *
	 * @param int $limit The maximum number of unimported objects to be returned. Not used, exists to comply with the interface.
	 *
	 * @return int 1 if it hasn't been completed in the past, 0 if it has.
	 */
	public function get_limited_unindexed_count( $limit ) {
		return ( ! $this->get_completed() ) ? 1 : 0;
	}

	/**
	 * Validates AIOSEO data.
	 *
	 * @return array An array of validated data or false if aioseo data did not pass validation.
	 *
	 * @throws Aioseo_Validation_Exception If the validation fails.
	 */
	public function index() {
		if ( $this->get_completed() ) {
			return [];
		}

		$validated_aioseo_table    = $this->validate_aioseo_table();
		$validated_aioseo_settings = $this->validate_aioseo_settings();
		$validated_robot_settings  = $this->validate_robot_settings();

		if ( $validated_aioseo_table === false || $validated_aioseo_settings === false || $validated_robot_settings === false ) {
			throw new Aioseo_Validation_Exception();
		}

		$this->set_completed( true );

		return [
			'validated_aioseo_table'    => $validated_aioseo_table,
			'validated_aioseo_settings' => $validated_aioseo_settings,
			'validated_robot_settings'  => $validated_robot_settings,
		];
	}

	/**
	 * Validates the AIOSEO indexable table.
	 *
	 * @return bool Whether the AIOSEO table exists and has the structure we expect.
	 */
	public function validate_aioseo_table() {
		if ( ! $this->aioseo_helper->aioseo_exists() ) {
			return false;
		}

		$table       = $this->aioseo_helper->get_table();
		$needed_data = $this->post_importing_action->get_needed_data();

		$aioseo_columns = $this->wpdb->get_col(
			"SHOW COLUMNS FROM {$table}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
			0
		);

		return $needed_data === \array_intersect( $needed_data, $aioseo_columns );
	}

	/**
	 * Validates the AIOSEO settings from the options table.
	 *
	 * @return bool Whether the AIOSEO settings from the options table exist and have the structure we expect.
	 */
	public function validate_aioseo_settings() {
		foreach ( $this->settings_importing_actions as $settings_import_action ) {
			$aioseo_settings = \json_decode( \get_option( $settings_import_action->get_source_option_name(), '' ), true );

			if ( ! $settings_import_action->isset_settings_tab( $aioseo_settings ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Validates the AIOSEO robots settings from the options table.
	 *
	 * @return bool Whether the AIOSEO robots settings from the options table exist and have the structure we expect.
	 */
	public function validate_robot_settings() {
		if ( $this->validate_post_robot_settings() && $this->validate_default_robot_settings() ) {
			return true;
		}

		return false;
	}

	/**
	 * Validates the post AIOSEO robots settings from the options table.
	 *
	 * @return bool Whether the post AIOSEO robots settings from the options table exist and have the structure we expect.
	 */
	public function validate_post_robot_settings() {
		$post_robot_mapping = $this->post_importing_action->enhance_mapping();
		// We're gonna validate against posttype robot settings only for posts, assuming the robot settings stay the same for other post types.
		$post_robot_mapping['subtype'] = 'post';

		// Let's get both the aioseo_options and the aioseo_options_dynamic options.
		$aioseo_global_settings = $this->aioseo_helper->get_global_option();
		$aioseo_posts_settings  = \json_decode( \get_option( $post_robot_mapping['option_name'], '' ), true );

		$needed_robots_data = $this->post_importing_action->get_needed_robot_data();
		\array_push( $needed_robots_data, 'default', 'noindex' );

		foreach ( $needed_robots_data as $robot_setting ) {
			// Validate against global settings.
			if ( ! isset( $aioseo_global_settings['searchAppearance']['advanced']['globalRobotsMeta'][ $robot_setting ] ) ) {
				return false;
			}

			// Validate against posttype settings.
			if ( ! isset( $aioseo_posts_settings['searchAppearance'][ $post_robot_mapping['type'] ][ $post_robot_mapping['subtype'] ]['advanced']['robotsMeta'][ $robot_setting ] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Validates the default AIOSEO robots settings for search appearance settings from the options table.
	 *
	 * @return bool Whether the AIOSEO robots settings for search appearance settings from the options table exist and have the structure we expect.
	 */
	public function validate_default_robot_settings() {

		foreach ( $this->settings_importing_actions as $settings_import_action ) {
			$robot_setting_map = $settings_import_action->pluck_robot_setting_from_mapping();

			// Some actions return empty robot settings, let's not validate against those.
			if ( ! empty( $robot_setting_map ) ) {
				$aioseo_settings = \json_decode( \get_option( $robot_setting_map['option_name'], '' ), true );

				if ( ! isset( $aioseo_settings['searchAppearance'][ $robot_setting_map['type'] ][ $robot_setting_map['subtype'] ]['advanced']['robotsMeta']['default'] ) ) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Used nowhere. Exists to comply with the interface.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of validations during each action pass.
		 *
		 * @param int $limit The maximum number of validations.
		 */
		$limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}
}
importing/aioseo/abstract-aioseo-settings-importing-action.php000064400000024610147511254640020757 0ustar00<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Given it's a very specific case.
namespace Yoast\WP\SEO\Actions\Importing\Aioseo;

use Exception;
use Yoast\WP\SEO\Actions\Importing\Abstract_Aioseo_Importing_Action;
use Yoast\WP\SEO\Helpers\Import_Helper;

/**
 * Abstract class for importing AIOSEO settings.
 */
abstract class Abstract_Aioseo_Settings_Importing_Action extends Abstract_Aioseo_Importing_Action {

	/**
	 * The plugin the class deals with.
	 *
	 * @var string
	 */
	public const PLUGIN = null;

	/**
	 * The type the class deals with.
	 *
	 * @var string
	 */
	public const TYPE = null;

	/**
	 * The option_name of the AIOSEO option that contains the settings.
	 */
	public const SOURCE_OPTION_NAME = null;

	/**
	 * The map of aioseo_options to yoast settings.
	 *
	 * @var array
	 */
	protected $aioseo_options_to_yoast_map = [];

	/**
	 * The tab of the aioseo settings we're working with, eg. taxonomies, posttypes.
	 *
	 * @var string
	 */
	protected $settings_tab = '';

	/**
	 * Additional mapping between AiOSEO replace vars and Yoast replace vars.
	 *
	 * @var array
	 *
	 * @see https://yoast.com/help/list-available-snippet-variables-yoast-seo/
	 */
	protected $replace_vars_edited_map = [];

	/**
	 * The import helper.
	 *
	 * @var Import_Helper
	 */
	protected $import_helper;

	/**
	 * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method.
	 *
	 * @return void
	 */
	abstract protected function build_mapping();

	/**
	 * Sets the import helper.
	 *
	 * @required
	 *
	 * @param Import_Helper $import_helper The import helper.
	 *
	 * @return void
	 */
	public function set_import_helper( Import_Helper $import_helper ) {
		$this->import_helper = $import_helper;
	}

	/**
	 * Retrieves the source option_name.
	 *
	 * @return string The source option_name.
	 *
	 * @throws Exception If the SOURCE_OPTION_NAME constant is not set in the child class.
	 */
	public function get_source_option_name() {
		$source_option_name = static::SOURCE_OPTION_NAME;

		if ( empty( $source_option_name ) ) {
			throw new Exception( 'Importing settings action without explicit source option_name' );
		}

		return $source_option_name;
	}

	/**
	 * Returns the total number of unimported objects.
	 *
	 * @return int The total number of unimported objects.
	 */
	public function get_total_unindexed() {
		return $this->get_unindexed_count();
	}

	/**
	 * Returns the limited number of unimported objects.
	 *
	 * @param int $limit The maximum number of unimported objects to be returned.
	 *
	 * @return int The limited number of unindexed posts.
	 */
	public function get_limited_unindexed_count( $limit ) {
		return $this->get_unindexed_count( $limit );
	}

	/**
	 * Returns the number of unimported objects (limited if limit is applied).
	 *
	 * @param int|null $limit The maximum number of unimported objects to be returned.
	 *
	 * @return int The number of unindexed posts.
	 */
	protected function get_unindexed_count( $limit = null ) {
		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = null;
		}

		$settings_to_create = $this->query( $limit );

		$number_of_settings_to_create = \count( $settings_to_create );
		$completed                    = $number_of_settings_to_create === 0;
		$this->set_completed( $completed );

		return $number_of_settings_to_create;
	}

	/**
	 * Imports AIOSEO settings.
	 *
	 * @return array|false An array of the AIOSEO settings that were imported or false if aioseo data was not found.
	 */
	public function index() {
		$limit            = $this->get_limit();
		$aioseo_settings  = $this->query( $limit );
		$created_settings = [];

		$completed = \count( $aioseo_settings ) === 0;
		$this->set_completed( $completed );

		// Prepare the setting keys mapping.
		$this->build_mapping();

		// Prepare the replacement var mapping.
		foreach ( $this->replace_vars_edited_map as $aioseo_var => $yoast_var ) {
			$this->replacevar_handler->compose_map( $aioseo_var, $yoast_var );
		}

		$last_imported_setting = '';
		try {
			foreach ( $aioseo_settings as $setting => $setting_value ) {
				// Map and import the values of the setting we're working with (eg. post, book-category, etc.) to the respective Yoast option.
				$this->map( $setting_value, $setting );

				// Save the type of the settings that were just imported, so that we can allow chunked imports.
				$last_imported_setting = $setting;

				$created_settings[] = $setting;
			}
		}
		finally {
			$cursor_id = $this->get_cursor_id();
			$this->import_cursor->set_cursor( $cursor_id, $last_imported_setting );
		}

		return $created_settings;
	}

	/**
	 * Checks if the settings tab subsetting is set in the AIOSEO option.
	 *
	 * @param string $aioseo_settings The AIOSEO option.
	 *
	 * @return bool Whether the settings are set.
	 */
	public function isset_settings_tab( $aioseo_settings ) {
		return isset( $aioseo_settings['searchAppearance'][ $this->settings_tab ] );
	}

	/**
	 * Queries the database and retrieves unimported AiOSEO settings (in chunks if a limit is applied).
	 *
	 * @param int|null $limit The maximum number of unimported objects to be returned.
	 *
	 * @return array The (maybe chunked) unimported AiOSEO settings to import.
	 */
	protected function query( $limit = null ) {
		$aioseo_settings = \json_decode( \get_option( $this->get_source_option_name(), '' ), true );

		if ( empty( $aioseo_settings ) ) {
			return [];
		}

		// We specifically want the setttings of the tab we're working with, eg. postTypes, taxonomies, etc.
		$settings_values = $aioseo_settings['searchAppearance'][ $this->settings_tab ];
		if ( ! \is_array( $settings_values ) ) {
			return [];
		}

		$flattened_settings = $this->import_helper->flatten_settings( $settings_values );

		return $this->get_unimported_chunk( $flattened_settings, $limit );
	}

	/**
	 * Retrieves (a chunk of, if limit is applied) the unimported AIOSEO settings.
	 * To apply a chunk, we manipulate the cursor to the keys of the AIOSEO settings.
	 *
	 * @param array $importable_data All of the available AIOSEO settings.
	 * @param int   $limit           The maximum number of unimported objects to be returned.
	 *
	 * @return array The (chunk of, if limit is applied)) unimported AIOSEO settings.
	 */
	protected function get_unimported_chunk( $importable_data, $limit ) {
		\ksort( $importable_data );

		$cursor_id = $this->get_cursor_id();
		$cursor    = $this->import_cursor->get_cursor( $cursor_id, '' );

		/**
		 * Filter 'wpseo_aioseo_<identifier>_import_cursor' - Allow filtering the value of the aioseo settings import cursor.
		 *
		 * @param int $import_cursor The value of the aioseo posttype default settings import cursor.
		 */
		$cursor = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_import_cursor', $cursor );

		if ( $cursor === '' ) {
			return \array_slice( $importable_data, 0, $limit, true );
		}

		// Let's find the position of the cursor in the alphabetically sorted importable data, so we can return only the unimported data.
		$keys = \array_flip( \array_keys( $importable_data ) );
		// If the stored cursor now no longer exists in the data, we have no choice but to start over.
		$position = ( isset( $keys[ $cursor ] ) ) ? ( $keys[ $cursor ] + 1 ) : 0;

		return \array_slice( $importable_data, $position, $limit, true );
	}

	/**
	 * Returns the number of objects that will be imported in a single importing pass.
	 *
	 * @return int The limit.
	 */
	public function get_limit() {
		/**
		 * Filter 'wpseo_aioseo_<identifier>_indexation_limit' - Allow filtering the number of settings imported during each importing pass.
		 *
		 * @param int $max_posts The maximum number of posts indexed.
		 */
		$limit = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_indexation_limit', 25 );

		if ( ! \is_int( $limit ) || $limit < 1 ) {
			$limit = 25;
		}

		return $limit;
	}

	/**
	 * Maps/imports AIOSEO settings into the respective Yoast settings.
	 *
	 * @param string|array $setting_value The value of the AIOSEO setting at hand.
	 * @param string       $setting       The setting at hand, eg. post or movie-category, separator etc.
	 *
	 * @return void
	 */
	protected function map( $setting_value, $setting ) {
		$aioseo_options_to_yoast_map = $this->aioseo_options_to_yoast_map;

		if ( isset( $aioseo_options_to_yoast_map[ $setting ] ) ) {
			$this->import_single_setting( $setting, $setting_value, $aioseo_options_to_yoast_map[ $setting ] );
		}
	}

	/**
	 * Imports a single setting in the db after transforming it to adhere to Yoast conventions.
	 *
	 * @param string $setting         The name of the setting.
	 * @param string $setting_value   The values of the setting.
	 * @param array  $setting_mapping The mapping of the setting to Yoast formats.
	 *
	 * @return void
	 */
	protected function import_single_setting( $setting, $setting_value, $setting_mapping ) {
		$yoast_key = $setting_mapping['yoast_name'];

		// Check if we're supposed to save the setting.
		if ( $this->options->get_default( 'wpseo_titles', $yoast_key ) !== null ) {
			// Then, do any needed data transfomation before actually saving the incoming data.
			$transformed_data = \call_user_func( [ $this, $setting_mapping['transform_method'] ], $setting_value, $setting_mapping );

			$this->options->set( $yoast_key, $transformed_data );
		}
	}

	/**
	 * Minimally transforms boolean data to be imported.
	 *
	 * @param bool $meta_data The boolean meta data to be imported.
	 *
	 * @return bool The transformed boolean meta data.
	 */
	public function simple_boolean_import( $meta_data ) {
		return $meta_data;
	}

	/**
	 * Imports the noindex setting, taking into consideration whether they defer to global defaults.
	 *
	 * @param bool  $noindex The noindex of the type, without taking into consideration whether the type defers to global defaults.
	 * @param array $mapping The mapping of the setting we're working with.
	 *
	 * @return bool The noindex setting.
	 */
	public function import_noindex( $noindex, $mapping ) {
		return $this->robots_transformer->transform_robot_setting( 'noindex', $noindex, $mapping );
	}

	/**
	 * Returns a setting map of the robot setting for one subset of post types/taxonomies/archives.
	 * For custom archives, it returns an empty array because AIOSEO excludes some custom archives from this option structure, eg. WooCommerce's products and we don't want to raise a false alarm.
	 *
	 * @return array The setting map of the robot setting for one subset of post types/taxonomies/archives or an empty array.
	 */
	public function pluck_robot_setting_from_mapping() {
		return [];
	}
}
importing/importing-action-interface.php000064400000001614147511254640014521 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Importing;

use Yoast\WP\SEO\Actions\Indexing\Limited_Indexing_Action_Interface;

interface Importing_Action_Interface extends Importing_Indexation_Action_Interface, Limited_Indexing_Action_Interface {

	/**
	 * Returns the name of the plugin we import from.
	 *
	 * @return string The plugin name.
	 */
	public function get_plugin();

	/**
	 * Returns the type of data we import.
	 *
	 * @return string The type of data.
	 */
	public function get_type();

	/**
	 * Whether or not this action is capable of importing given a specific plugin and type.
	 *
	 * @param string|null $plugin The name of the plugin being imported.
	 * @param string|null $type   The component of the plugin being imported.
	 *
	 * @return bool True if the action can import the given plugin's data of the given type.
	 */
	public function is_compatible_with( $plugin = null, $type = null );
}
addon-installation/addon-install-action.php000064400000007562147511254640015070 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Addon_Installation;

use Plugin_Upgrader;
use WP_Error;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Already_Installed_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Installation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Install_Plugins_Exception;
use Yoast\WP\SEO\Helpers\Require_File_Helper;

/**
 * Represents the endpoint for downloading and installing a zip-file from MyYoast.
 */
class Addon_Install_Action {

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The require file helper.
	 *
	 * @var Require_File_Helper
	 */
	protected $require_file_helper;

	/**
	 * Addon_Activate_Action constructor.
	 *
	 * @param WPSEO_Addon_Manager $addon_manager       The addon manager.
	 * @param Require_File_Helper $require_file_helper A helper that can require files.
	 */
	public function __construct(
		WPSEO_Addon_Manager $addon_manager,
		Require_File_Helper $require_file_helper
	) {
		$this->addon_manager       = $addon_manager;
		$this->require_file_helper = $require_file_helper;
	}

	/**
	 * Installs the plugin based on the given slug.
	 *
	 * @param string $plugin_slug  The plugin slug to install.
	 * @param string $download_url The plugin download URL.
	 *
	 * @return bool True when install is successful.
	 *
	 * @throws Addon_Already_Installed_Exception  When the addon is already installed.
	 * @throws Addon_Installation_Error_Exception When the installation encounters an error.
	 * @throws User_Cannot_Install_Plugins_Exception        When the user does not have the permissions to install plugins.
	 */
	public function install_addon( $plugin_slug, $download_url ) {
		if ( ! \current_user_can( 'install_plugins' ) ) {
			throw new User_Cannot_Install_Plugins_Exception( $plugin_slug );
		}

		if ( $this->is_installed( $plugin_slug ) ) {
			throw new Addon_Already_Installed_Exception( $plugin_slug );
		}

		$this->load_wordpress_classes();

		$install_result = $this->install( $download_url );
		if ( \is_wp_error( $install_result ) ) {
			throw new Addon_Installation_Error_Exception( $install_result->get_error_message() );
		}

		return $install_result;
	}

	/**
	 * Requires the files needed from WordPress itself.
	 *
	 * @codeCoverageIgnore
	 *
	 * @return void
	 */
	protected function load_wordpress_classes() {
		if ( ! \class_exists( 'WP_Upgrader' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
		}

		if ( ! \class_exists( 'Plugin_Upgrader' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' );
		}

		if ( ! \class_exists( 'WP_Upgrader_Skin' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' );
		}

		if ( ! \function_exists( 'get_plugin_data' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' );
		}

		if ( ! \function_exists( 'request_filesystem_credentials' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/file.php' );
		}
	}

	/**
	 * Checks is a plugin is installed.
	 *
	 * @param string $plugin_slug The plugin to check.
	 *
	 * @return bool True when plugin is installed.
	 */
	protected function is_installed( $plugin_slug ) {
		return $this->addon_manager->get_plugin_file( $plugin_slug ) !== false;
	}

	/**
	 * Runs the installation by using the WordPress installation routine.
	 *
	 * @codeCoverageIgnore Contains WordPress specific logic.
	 *
	 * @param string $plugin_download The url to the download.
	 *
	 * @return bool|WP_Error True when success, WP_Error when something went wrong.
	 */
	protected function install( $plugin_download ) {
		$plugin_upgrader = new Plugin_Upgrader();

		return $plugin_upgrader->install( $plugin_download );
	}
}
addon-installation/addon-activate-action.php000064400000004522147511254640015213 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Addon_Installation;

use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Exceptions\Addon_Installation\Addon_Activation_Error_Exception;
use Yoast\WP\SEO\Exceptions\Addon_Installation\User_Cannot_Activate_Plugins_Exception;
use Yoast\WP\SEO\Helpers\Require_File_Helper;

/**
 * Represents the endpoint for activating a specific Yoast Plugin on WordPress.
 */
class Addon_Activate_Action {

	/**
	 * The addon manager.
	 *
	 * @var WPSEO_Addon_Manager
	 */
	protected $addon_manager;

	/**
	 * The require file helper.
	 *
	 * @var Require_File_Helper
	 */
	protected $require_file_helper;

	/**
	 * Addon_Activate_Action constructor.
	 *
	 * @param WPSEO_Addon_Manager $addon_manager       The addon manager.
	 * @param Require_File_Helper $require_file_helper A file helper.
	 */
	public function __construct(
		WPSEO_Addon_Manager $addon_manager,
		Require_File_Helper $require_file_helper
	) {
		$this->addon_manager       = $addon_manager;
		$this->require_file_helper = $require_file_helper;
	}

	/**
	 * Activates the plugin based on the given plugin file.
	 *
	 * @param string $plugin_slug The plugin slug to get download url for.
	 *
	 * @return bool True when activation is successful.
	 *
	 * @throws Addon_Activation_Error_Exception       Exception when the activation encounters an error.
	 * @throws User_Cannot_Activate_Plugins_Exception Exception when the user is not allowed to activate.
	 */
	public function activate_addon( $plugin_slug ) {
		if ( ! \current_user_can( 'activate_plugins' ) ) {
			throw new User_Cannot_Activate_Plugins_Exception();
		}

		if ( $this->addon_manager->is_installed( $plugin_slug ) ) {
			return true;
		}

		$this->load_wordpress_classes();

		$plugin_file       = $this->addon_manager->get_plugin_file( $plugin_slug );
		$activation_result = \activate_plugin( $plugin_file );

		if ( $activation_result !== null && \is_wp_error( $activation_result ) ) {
			throw new Addon_Activation_Error_Exception( $activation_result->get_error_message() );
		}

		return true;
	}

	/**
	 * Requires the files needed from WordPress itself.
	 *
	 * @codeCoverageIgnore Only loads a WordPress file.
	 *
	 * @return void
	 */
	protected function load_wordpress_classes() {
		if ( ! \function_exists( 'get_plugins' ) ) {
			$this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' );
		}
	}
}
semrush/semrush-options-action.php000064400000002140147511254640013403 0ustar00<?php

namespace Yoast\WP\SEO\Actions\SEMrush;

use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Class SEMrush_Options_Action
 */
class SEMrush_Options_Action {

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * SEMrush_Options_Action constructor.
	 *
	 * @param Options_Helper $options_helper The WPSEO options helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

	/**
	 * Stores SEMrush country code in the WPSEO options.
	 *
	 * @param string $country_code The country code to store.
	 *
	 * @return object The response object.
	 */
	public function set_country_code( $country_code ) {
		// The country code has already been validated at this point. No need to do that again.
		$success = $this->options_helper->set( 'semrush_country_code', $country_code );

		if ( $success ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}
		return (object) [
			'success' => false,
			'status'  => 500,
			'error'   => 'Could not save option in the database',
		];
	}
}
semrush/semrush-phrases-action.php000064400000004441147511254640013363 0ustar00<?php

namespace Yoast\WP\SEO\Actions\SEMrush;

use Exception;
use Yoast\WP\SEO\Config\SEMrush_Client;

/**
 * Class SEMrush_Phrases_Action
 */
class SEMrush_Phrases_Action {

	/**
	 * The transient cache key.
	 */
	public const TRANSIENT_CACHE_KEY = 'wpseo_semrush_related_keyphrases_%s_%s';

	/**
	 * The SEMrush keyphrase URL.
	 *
	 * @var string
	 */
	public const KEYPHRASES_URL = 'https://oauth.semrush.com/api/v1/keywords/phrase_fullsearch';

	/**
	 * The SEMrush_Client instance.
	 *
	 * @var SEMrush_Client
	 */
	protected $client;

	/**
	 * SEMrush_Phrases_Action constructor.
	 *
	 * @param SEMrush_Client $client The API client.
	 */
	public function __construct( SEMrush_Client $client ) {
		$this->client = $client;
	}

	/**
	 * Gets the related keyphrases and data based on the passed keyphrase and database country code.
	 *
	 * @param string $keyphrase The keyphrase to search for.
	 * @param string $database  The database's country code.
	 *
	 * @return object The response object.
	 */
	public function get_related_keyphrases( $keyphrase, $database ) {
		try {
			$transient_key = \sprintf( static::TRANSIENT_CACHE_KEY, $keyphrase, $database );
			$transient     = \get_transient( $transient_key );

			if ( $transient !== false && isset( $transient['data']['columnNames'] ) && \count( $transient['data']['columnNames'] ) === 5 ) {
				return $this->to_result_object( $transient );
			}

			$options = [
				'params' => [
					'phrase'         => $keyphrase,
					'database'       => $database,
					'export_columns' => 'Ph,Nq,Td,In,Kd',
					'display_limit'  => 10,
					'display_offset' => 0,
					'display_sort'   => 'nq_desc',
					'display_filter' => '%2B|Nq|Lt|1000',
				],
			];

			$results = $this->client->get( self::KEYPHRASES_URL, $options );

			\set_transient( $transient_key, $results, \DAY_IN_SECONDS );

			return $this->to_result_object( $results );
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Converts the passed dataset to an object.
	 *
	 * @param array $result The result dataset to convert to an object.
	 *
	 * @return object The result object.
	 */
	protected function to_result_object( $result ) {
		return (object) [
			'results' => $result['data'],
			'status'  => $result['status'],
		];
	}
}
semrush/semrush-login-action.php000064400000002360147511254640013024 0ustar00<?php

namespace Yoast\WP\SEO\Actions\SEMrush;

use Yoast\WP\SEO\Config\SEMrush_Client;
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;

/**
 * Class SEMrush_Login_Action
 */
class SEMrush_Login_Action {

	/**
	 * The SEMrush_Client instance.
	 *
	 * @var SEMrush_Client
	 */
	protected $client;

	/**
	 * SEMrush_Login_Action constructor.
	 *
	 * @param SEMrush_Client $client The API client.
	 */
	public function __construct( SEMrush_Client $client ) {
		$this->client = $client;
	}

	/**
	 * Authenticates with SEMrush to request the necessary tokens.
	 *
	 * @param string $code The authentication code to use to request a token with.
	 *
	 * @return object The response object.
	 */
	public function authenticate( $code ) {
		// Code has already been validated at this point. No need to do that again.
		try {
			$tokens = $this->client->request_tokens( $code );

			return (object) [
				'tokens' => $tokens->to_array(),
				'status' => 200,
			];
		} catch ( Authentication_Failed_Exception $e ) {
			return $e->get_response();
		}
	}

	/**
	 * Performs the login request, if necessary.
	 *
	 * @return void
	 */
	public function login() {
		if ( $this->client->has_valid_tokens() ) {
			return;
		}

		// Prompt with login screen.
	}
}
integrations-action.php000064400000002366147511254640011256 0ustar00<?php

namespace Yoast\WP\SEO\Actions;

use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Class Integrations_Action.
 */
class Integrations_Action {

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Integrations_Action constructor.
	 *
	 * @param Options_Helper $options_helper The WPSEO options helper.
	 */
	public function __construct( Options_Helper $options_helper ) {
		$this->options_helper = $options_helper;
	}

	/**
	 * Sets an integration state.
	 *
	 * @param string $integration_name The name of the integration to activate/deactivate.
	 * @param bool   $value            The value to store.
	 *
	 * @return object The response object.
	 */
	public function set_integration_active( $integration_name, $value ) {
		$option_name  = $integration_name . '_integration_active';
		$success      = true;
		$option_value = $this->options_helper->get( $option_name );

		if ( $option_value !== $value ) {
			$success = $this->options_helper->set( $option_name, $value );
		}

		if ( $success ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}
		return (object) [
			'success' => false,
			'status'  => 500,
			'error'   => 'Could not save the option in the database',
		];
	}
}
wincher/wincher-keyphrases-action.php000064400000022227147511254640014020 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Wincher;

use Exception;
use WP_Post;
use WPSEO_Utils;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;

/**
 * Class Wincher_Keyphrases_Action
 */
class Wincher_Keyphrases_Action {

	/**
	 * The Wincher keyphrase URL for bulk addition.
	 *
	 * @var string
	 */
	public const KEYPHRASES_ADD_URL = 'https://api.wincher.com/beta/websites/%s/keywords/bulk';

	/**
	 * The Wincher tracked keyphrase retrieval URL.
	 *
	 * @var string
	 */
	public const KEYPHRASES_URL = 'https://api.wincher.com/beta/yoast/%s';

	/**
	 * The Wincher delete tracked keyphrase URL.
	 *
	 * @var string
	 */
	public const KEYPHRASE_DELETE_URL = 'https://api.wincher.com/beta/websites/%s/keywords/%s';

	/**
	 * The Wincher_Client instance.
	 *
	 * @var Wincher_Client
	 */
	protected $client;

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The Indexable_Repository instance.
	 *
	 * @var Indexable_Repository
	 */
	protected $indexable_repository;

	/**
	 * Wincher_Keyphrases_Action constructor.
	 *
	 * @param Wincher_Client       $client               The API client.
	 * @param Options_Helper       $options_helper       The options helper.
	 * @param Indexable_Repository $indexable_repository The indexables repository.
	 */
	public function __construct(
		Wincher_Client $client,
		Options_Helper $options_helper,
		Indexable_Repository $indexable_repository
	) {
		$this->client               = $client;
		$this->options_helper       = $options_helper;
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Sends the tracking API request for one or more keyphrases.
	 *
	 * @param string|array $keyphrases One or more keyphrases that should be tracked.
	 * @param Object       $limits     The limits API call response data.
	 *
	 * @return Object The reponse object.
	 */
	public function track_keyphrases( $keyphrases, $limits ) {
		try {
			$endpoint = \sprintf(
				self::KEYPHRASES_ADD_URL,
				$this->options_helper->get( 'wincher_website_id' )
			);

			// Enforce arrrays to ensure a consistent way of preparing the request.
			if ( ! \is_array( $keyphrases ) ) {
				$keyphrases = [ $keyphrases ];
			}

			// Calculate if the user would exceed their limit.
			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored.
			if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) {
				$response = [
					'limit'  => $limits->limit,
					'error'  => 'Account limit exceeded',
					'status' => 400,
				];

				return $this->to_result_object( $response );
			}

			$formatted_keyphrases = \array_values(
				\array_map(
					static function ( $keyphrase ) {
						return [
							'keyword' => $keyphrase,
							'groups'  => [],
						];
					},
					$keyphrases
				)
			);

			$results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) );

			if ( ! \array_key_exists( 'data', $results ) ) {
				return $this->to_result_object( $results );
			}

			// The endpoint returns a lot of stuff that we don't want/need.
			$results['data'] = \array_map(
				static function ( $keyphrase ) {
					return [
						'id'         => $keyphrase['id'],
						'keyword'    => $keyphrase['keyword'],
					];
				},
				$results['data']
			);

			$results['data'] = \array_combine(
				\array_column( $results['data'], 'keyword' ),
				\array_values( $results['data'] )
			);

			return $this->to_result_object( $results );
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Sends an untrack request for the passed keyword ID.
	 *
	 * @param int $keyphrase_id The ID of the keyphrase to untrack.
	 *
	 * @return object The response object.
	 */
	public function untrack_keyphrase( $keyphrase_id ) {
		try {
			$endpoint = \sprintf(
				self::KEYPHRASE_DELETE_URL,
				$this->options_helper->get( 'wincher_website_id' ),
				$keyphrase_id
			);

			$this->client->delete( $endpoint );

			return (object) [
				'status' => 200,
			];
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Gets the keyphrase data for the passed keyphrases.
	 * Retrieves all available data if no keyphrases are provided.
	 *
	 * @param array|null  $used_keyphrases The currently used keyphrases. Optional.
	 * @param string|null $permalink       The current permalink. Optional.
	 * @param string|null $start_at        The position start date. Optional.
	 *
	 * @return object The keyphrase chart data.
	 */
	public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null, $start_at = null ) {
		try {
			if ( $used_keyphrases === null ) {
				$used_keyphrases = $this->collect_all_keyphrases();
			}

			// If we still have no keyphrases the API will return an error, so
			// don't even bother sending a request.
			if ( empty( $used_keyphrases ) ) {
				return $this->to_result_object(
					[
						'data'   => [],
						'status' => 200,
					]
				);
			}

			$endpoint = \sprintf(
				self::KEYPHRASES_URL,
				$this->options_helper->get( 'wincher_website_id' )
			);

			$results = $this->client->post(
				$endpoint,
				WPSEO_Utils::format_json_encode(
					[
						'keywords' => $used_keyphrases,
						'url'      => $permalink,
						'start_at' => $start_at,
					]
				),
				[
					'timeout' => 60,
				]
			);

			if ( ! \array_key_exists( 'data', $results ) ) {
				return $this->to_result_object( $results );
			}

			$results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases );

			// Extract the positional data and assign it to the keyphrase.
			$results['data'] = \array_combine(
				\array_column( $results['data'], 'keyword' ),
				\array_values( $results['data'] )
			);

			return $this->to_result_object( $results );
		} catch ( Exception $e ) {
			return (object) [
				'error'  => $e->getMessage(),
				'status' => $e->getCode(),
			];
		}
	}

	/**
	 * Collects the keyphrases associated with the post.
	 *
	 * @param WP_Post $post The post object.
	 *
	 * @return array The keyphrases.
	 */
	public function collect_keyphrases_from_post( $post ) {
		$keyphrases        = [];
		$primary_keyphrase = $this->indexable_repository
			->query()
			->select( 'primary_focus_keyword' )
			->where( 'object_id', $post->ID )
			->find_one();

		if ( $primary_keyphrase ) {
			$keyphrases[] = $primary_keyphrase->primary_focus_keyword;
		}

		/**
		 * Filters the keyphrases collected by the Wincher integration from the post.
		 *
		 * @param array $keyphrases The keyphrases array.
		 * @param int   $post_id    The ID of the post.
		 */
		return \apply_filters( 'wpseo_wincher_keyphrases_from_post', $keyphrases, $post->ID );
	}

	/**
	 * Collects all keyphrases known to Yoast.
	 *
	 * @return array
	 */
	protected function collect_all_keyphrases() {
		// Collect primary keyphrases first.
		$keyphrases = \array_column(
			$this->indexable_repository
				->query()
				->select( 'primary_focus_keyword' )
				->where_not_null( 'primary_focus_keyword' )
				->where( 'object_type', 'post' )
				->where_not_equal( 'post_status', 'trash' )
				->distinct()
				->find_array(),
			'primary_focus_keyword'
		);

		/**
		 * Filters the keyphrases collected by the Wincher integration from all the posts.
		 *
		 * @param array $keyphrases The keyphrases array.
		 */
		$keyphrases = \apply_filters( 'wpseo_wincher_all_keyphrases', $keyphrases );

		// Filter out empty entries.
		return \array_filter( $keyphrases );
	}

	/**
	 * Filters the results based on the passed keyphrases.
	 *
	 * @param array $results         The results to filter.
	 * @param array $used_keyphrases The used keyphrases.
	 *
	 * @return array The filtered results.
	 */
	protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) {
		return \array_filter(
			$results,
			static function ( $result ) use ( $used_keyphrases ) {
				return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true );
			}
		);
	}

	/**
	 * Determines whether the amount of keyphrases would mean the user exceeds their account limits.
	 *
	 * @param string|array $keyphrases The keyphrases to be added.
	 * @param object       $limits     The current account limits.
	 *
	 * @return bool Whether the limit is exceeded.
	 */
	protected function would_exceed_limits( $keyphrases, $limits ) {
		if ( ! \is_array( $keyphrases ) ) {
			$keyphrases = [ $keyphrases ];
		}

		if ( \is_null( $limits->limit ) ) {
			return false;
		}

		return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit;
	}

	/**
	 * Converts the passed dataset to an object.
	 *
	 * @param array $result The result dataset to convert to an object.
	 *
	 * @return object The result object.
	 */
	protected function to_result_object( $result ) {
		if ( \array_key_exists( 'data', $result ) ) {
			$result['results'] = (object) $result['data'];

			unset( $result['data'] );
		}

		if ( \array_key_exists( 'message', $result ) ) {
			$result['error'] = $result['message'];

			unset( $result['message'] );
		}

		return (object) $result;
	}
}
wincher/wincher-login-action.php000064400000003336147511254640012752 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Wincher;

use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Class Wincher_Login_Action
 */
class Wincher_Login_Action {

	/**
	 * The Wincher_Client instance.
	 *
	 * @var Wincher_Client
	 */
	protected $client;

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Wincher_Login_Action constructor.
	 *
	 * @param Wincher_Client $client         The API client.
	 * @param Options_Helper $options_helper The options helper.
	 */
	public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
		$this->client         = $client;
		$this->options_helper = $options_helper;
	}

	/**
	 * Returns the authorization URL.
	 *
	 * @return object The response object.
	 */
	public function get_authorization_url() {
		return (object) [
			'status'    => 200,
			'url'       => $this->client->get_authorization_url(),
		];
	}

	/**
	 * Authenticates with Wincher to request the necessary tokens.
	 *
	 * @param string $code       The authentication code to use to request a token with.
	 * @param string $website_id The website id associated with the code.
	 *
	 * @return object The response object.
	 */
	public function authenticate( $code, $website_id ) {
		// Code has already been validated at this point. No need to do that again.
		try {
			$tokens = $this->client->request_tokens( $code );
			$this->options_helper->set( 'wincher_website_id', $website_id );

			return (object) [
				'tokens' => $tokens->to_array(),
				'status' => 200,
			];
		} catch ( Authentication_Failed_Exception $e ) {
			return $e->get_response();
		}
	}
}
wincher/wincher-account-action.php000064400000004743147511254640013301 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Wincher;

use Exception;
use Yoast\WP\SEO\Config\Wincher_Client;
use Yoast\WP\SEO\Helpers\Options_Helper;

/**
 * Class Wincher_Account_Action
 */
class Wincher_Account_Action {

	public const ACCOUNT_URL          = 'https://api.wincher.com/beta/account';
	public const UPGRADE_CAMPAIGN_URL = 'https://api.wincher.com/v1/yoast/upgrade-campaign';

	/**
	 * The Wincher_Client instance.
	 *
	 * @var Wincher_Client
	 */
	protected $client;

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * Wincher_Account_Action constructor.
	 *
	 * @param Wincher_Client $client         The API client.
	 * @param Options_Helper $options_helper The options helper.
	 */
	public function __construct( Wincher_Client $client, Options_Helper $options_helper ) {
		$this->client         = $client;
		$this->options_helper = $options_helper;
	}

	/**
	 * Checks the account limit for tracking keyphrases.
	 *
	 * @return object The response object.
	 */
	public function check_limit() {
		// Code has already been validated at this point. No need to do that again.
		try {
			$results = $this->client->get( self::ACCOUNT_URL );

			$usage   = $results['limits']['keywords']['usage'];
			$limit   = $results['limits']['keywords']['limit'];
			$history = $results['limits']['history_days'];

			return (object) [
				'canTrack'    => \is_null( $limit ) || $usage < $limit,
				'limit'       => $limit,
				'usage'       => $usage,
				'historyDays' => $history,
				'status'      => 200,
			];
		} catch ( Exception $e ) {
			return (object) [
				'status' => $e->getCode(),
				'error'  => $e->getMessage(),
			];
		}
	}

	/**
	 * Gets the upgrade campaign.
	 *
	 * @return object The response object.
	 */
	public function get_upgrade_campaign() {
		try {
			$result   = $this->client->get( self::UPGRADE_CAMPAIGN_URL );
			$type     = ( $result['type'] ?? null );
			$months   = ( $result['months'] ?? null );
			$discount = ( $result['value'] ?? null );

			// We display upgrade discount only if it's a rate discount and positive months/discount.
			if ( $type === 'RATE' && $months && $discount ) {

				return (object) [
					'discount'  => $discount,
					'months'    => $months,
					'status'    => 200,
				];
			}

			return (object) [
				'discount'  => null,
				'months'    => null,
				'status'    => 200,
			];
		} catch ( Exception $e ) {
			return (object) [
				'status' => $e->getCode(),
				'error'  => $e->getMessage(),
			];
		}
	}
}
configuration/first-time-configuration-action.php000064400000021147147511254640016345 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Configuration;

use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Social_Profiles_Helper;

/**
 * Class First_Time_Configuration_Action.
 */
class First_Time_Configuration_Action {

	/**
	 * The fields for the site representation payload.
	 */
	public const SITE_REPRESENTATION_FIELDS = [
		'company_or_person',
		'company_name',
		'website_name',
		'company_logo',
		'company_logo_id',
		'person_logo',
		'person_logo_id',
		'company_or_person_user_id',
		'description',
	];

	/**
	 * The Options_Helper instance.
	 *
	 * @var Options_Helper
	 */
	protected $options_helper;

	/**
	 * The Social_Profiles_Helper instance.
	 *
	 * @var Social_Profiles_Helper
	 */
	protected $social_profiles_helper;

	/**
	 * First_Time_Configuration_Action constructor.
	 *
	 * @param Options_Helper         $options_helper         The WPSEO options helper.
	 * @param Social_Profiles_Helper $social_profiles_helper The social profiles helper.
	 */
	public function __construct( Options_Helper $options_helper, Social_Profiles_Helper $social_profiles_helper ) {
		$this->options_helper         = $options_helper;
		$this->social_profiles_helper = $social_profiles_helper;
	}

	/**
	 * Stores the values for the site representation.
	 *
	 * @param array $params The values to store.
	 *
	 * @return object The response object.
	 */
	public function set_site_representation( $params ) {
		$failures   = [];
		$old_values = $this->get_old_values( self::SITE_REPRESENTATION_FIELDS );

		foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) {
			if ( isset( $params[ $field_name ] ) ) {
				$result = $this->options_helper->set( $field_name, $params[ $field_name ] );

				if ( ! $result ) {
					$failures[] = $field_name;
				}
			}
		}

		// Delete cached logos in the db.
		$this->options_helper->set( 'company_logo_meta', false );
		$this->options_helper->set( 'person_logo_meta', false );

		/**
		 * Action: 'wpseo_post_update_site_representation' - Allows for Hiive event tracking.
		 *
		 * @param array $params     The new values of the options.
		 * @param array $old_values The old values of the options.
		 * @param array $failures   The options that failed to be saved.
		 *
		 * @internal
		 */
		\do_action( 'wpseo_ftc_post_update_site_representation', $params, $old_values, $failures );

		if ( \count( $failures ) === 0 ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}

		return (object) [
			'success'  => false,
			'status'   => 500,
			'error'    => 'Could not save some options in the database',
			'failures' => $failures,
		];
	}

	/**
	 * Stores the values for the social profiles.
	 *
	 * @param array $params The values to store.
	 *
	 * @return object The response object.
	 */
	public function set_social_profiles( $params ) {
		$old_values = $this->get_old_values( \array_keys( $this->social_profiles_helper->get_organization_social_profile_fields() ) );
		$failures   = $this->social_profiles_helper->set_organization_social_profiles( $params );

		/**
		 * Action: 'wpseo_post_update_social_profiles' - Allows for Hiive event tracking.
		 *
		 * @param array $params     The new values of the options.
		 * @param array $old_values The old values of the options.
		 * @param array $failures   The options that failed to be saved.
		 *
		 * @internal
		 */
		\do_action( 'wpseo_ftc_post_update_social_profiles', $params, $old_values, $failures );

		if ( empty( $failures ) ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}

		return (object) [
			'success'  => false,
			'status'   => 200,
			'error'    => 'Could not save some options in the database',
			'failures' => $failures,
		];
	}

	/**
	 * Stores the values for the social profiles.
	 *
	 * @param array $params The values to store.
	 *
	 * @return object The response object.
	 */
	public function set_person_social_profiles( $params ) {
		$social_profiles = \array_filter(
			$params,
			static function ( $key ) {
				return $key !== 'user_id';
			},
			\ARRAY_FILTER_USE_KEY
		);

		$failures = $this->social_profiles_helper->set_person_social_profiles( $params['user_id'], $social_profiles );

		if ( \count( $failures ) === 0 ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}

		return (object) [
			'success'  => false,
			'status'   => 200,
			'error'    => 'Could not save some options in the database',
			'failures' => $failures,
		];
	}

	/**
	 * Gets the values for the social profiles.
	 *
	 * @param int $user_id The person ID.
	 *
	 * @return object The response object.
	 */
	public function get_person_social_profiles( $user_id ) {

		return (object) [
			'success'         => true,
			'status'          => 200,
			'social_profiles' => $this->social_profiles_helper->get_person_social_profiles( $user_id ),
		];
	}

	/**
	 * Stores the values to enable/disable tracking.
	 *
	 * @param array $params The values to store.
	 *
	 * @return object The response object.
	 */
	public function set_enable_tracking( $params ) {
		$success      = true;
		$option_value = $this->options_helper->get( 'tracking' );

		if ( $option_value !== $params['tracking'] ) {
			$this->options_helper->set( 'toggled_tracking', true );
			$success = $this->options_helper->set( 'tracking', $params['tracking'] );
		}

		/**
		 * Action: 'wpseo_post_update_enable_tracking' - Allows for Hiive event tracking.
		 *
		 * @param array $new_value The new value.
		 * @param array $old_value The old value.
		 * @param bool  $failure   Whether the option failed to be stored.
		 *
		 * @internal
		 */
		// $success is negated to be aligned with the other two actions which pass $failures.
		\do_action( 'wpseo_ftc_post_update_enable_tracking', $params['tracking'], $option_value, ! $success );

		if ( $success ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}

		return (object) [
			'success' => false,
			'status'  => 500,
			'error'   => 'Could not save the option in the database',
		];
	}

	/**
	 * Checks if the current user has the capability a specific user.
	 *
	 * @param int $user_id The id of the user to be edited.
	 *
	 * @return object The response object.
	 */
	public function check_capability( $user_id ) {
		if ( $this->can_edit_profile( $user_id ) ) {
			return (object) [
				'success' => true,
				'status'  => 200,
			];
		}

		return (object) [
			'success' => false,
			'status'  => 403,
		];
	}

	/**
	 * Stores the first time configuration state.
	 *
	 * @param array $params The values to store.
	 *
	 * @return object The response object.
	 */
	public function save_configuration_state( $params ) {
		// If the finishedSteps param is not present in the REST request, it's a malformed request.
		if ( ! isset( $params['finishedSteps'] ) ) {
			return (object) [
				'success' => false,
				'status'  => 400,
				'error'   => 'Bad request',
			];
		}

		// Sanitize input.
		$finished_steps = \array_map( '\sanitize_text_field', \wp_unslash( $params['finishedSteps'] ) );

		$success = $this->options_helper->set( 'configuration_finished_steps', $finished_steps );

		if ( ! $success ) {
			return (object) [
				'success' => false,
				'status'  => 500,
				'error'   => 'Could not save the option in the database',
			];
		}

		// If all the five steps of the configuration have been completed, set first_time_install option to false.
		if ( \count( $params['finishedSteps'] ) === 3 ) {
			$this->options_helper->set( 'first_time_install', false );
		}

		return (object) [
			'success' => true,
			'status'  => 200,
		];
	}

	/**
	 * Gets the first time configuration state.
	 *
	 * @return object The response object.
	 */
	public function get_configuration_state() {
		$configuration_option = $this->options_helper->get( 'configuration_finished_steps' );

		if ( ! \is_null( $configuration_option ) ) {
			return (object) [
				'success' => true,
				'status'  => 200,
				'data'    => $configuration_option,
			];
		}

		return (object) [
			'success' => false,
			'status'  => 500,
			'error'   => 'Could not get data from the database',
		];
	}

	/**
	 * Checks if the current user has the capability to edit a specific user.
	 *
	 * @param int $person_id The id of the person to edit.
	 *
	 * @return bool
	 */
	private function can_edit_profile( $person_id ) {
		return \current_user_can( 'edit_user', $person_id );
	}

	/**
	 * Gets the old values for the given fields.
	 *
	 * @param array $fields_names The fields to get the old values for.
	 *
	 * @return array The old values.
	 */
	private function get_old_values( array $fields_names ): array {
		$old_values = [];

		foreach ( $fields_names as $field_name ) {
			$old_values[ $field_name ] = $this->options_helper->get( $field_name );
		}

		return $old_values;
	}
}
indexables/indexable-head-action.php000064400000007417147511254640013522 0ustar00<?php

namespace Yoast\WP\SEO\Actions\Indexables;

use Yoast\WP\SEO\Surfaces\Meta_Surface;
use Yoast\WP\SEO\Surfaces\Values\Meta;

/**
 * Get head action for indexables.
 */
class Indexable_Head_Action {

	/**
	 * Caches the output.
	 *
	 * @var mixed
	 */
	protected $cache;

	/**
	 * The meta surface.
	 *
	 * @var Meta_Surface
	 */
	private $meta_surface;

	/**
	 * Indexable_Head_Action constructor.
	 *
	 * @param Meta_Surface $meta_surface The meta surface.
	 */
	public function __construct( Meta_Surface $meta_surface ) {
		$this->meta_surface = $meta_surface;
	}

	/**
	 * Retrieves the head for a url.
	 *
	 * @param string $url The url to get the head for.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_url( $url ) {
		if ( $url === \trailingslashit( \get_home_url() ) ) {
			return $this->with_404_fallback( $this->with_cache( 'home_page' ) );
		}
		return $this->with_404_fallback( $this->with_cache( 'url', $url ) );
	}

	/**
	 * Retrieves the head for a post.
	 *
	 * @param int $id The id.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_post( $id ) {
		return $this->with_404_fallback( $this->with_cache( 'post', $id ) );
	}

	/**
	 * Retrieves the head for a term.
	 *
	 * @param int $id The id.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_term( $id ) {
		return $this->with_404_fallback( $this->with_cache( 'term', $id ) );
	}

	/**
	 * Retrieves the head for an author.
	 *
	 * @param int $id The id.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_author( $id ) {
		return $this->with_404_fallback( $this->with_cache( 'author', $id ) );
	}

	/**
	 * Retrieves the head for a post type archive.
	 *
	 * @param int $type The id.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_post_type_archive( $type ) {
		return $this->with_404_fallback( $this->with_cache( 'post_type_archive', $type ) );
	}

	/**
	 * Retrieves the head for the posts page.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_posts_page() {
		return $this->with_404_fallback( $this->with_cache( 'posts_page' ) );
	}

	/**
	 * Retrieves the head for the 404 page. Always sets the status to 404.
	 *
	 * @return object Object with head and status properties.
	 */
	public function for_404() {
		$meta = $this->with_cache( '404' );

		if ( ! $meta ) {
			return (object) [
				'html'   => '',
				'json'   => [],
				'status' => 404,
			];
		}

		$head = $meta->get_head();

		return (object) [
			'html'   => $head->html,
			'json'   => $head->json,
			'status' => 404,
		];
	}

	/**
	 * Retrieves the head for a successful page load.
	 *
	 * @param object $head The calculated Yoast head.
	 *
	 * @return object The presentations and status code 200.
	 */
	protected function for_200( $head ) {
		return (object) [
			'html'   => $head->html,
			'json'   => $head->json,
			'status' => 200,
		];
	}

	/**
	 * Returns the head with 404 fallback
	 *
	 * @param Meta|false $meta The meta object.
	 *
	 * @return object The head response.
	 */
	protected function with_404_fallback( $meta ) {
		if ( $meta === false ) {
			return $this->for_404();
		}
		else {
			return $this->for_200( $meta->get_head() );
		}
	}

	/**
	 * Retrieves a value from the meta surface cached.
	 *
	 * @param string $type     The type of value to retrieve.
	 * @param string $argument Optional. The argument for the value.
	 *
	 * @return Meta The meta object.
	 */
	protected function with_cache( $type, $argument = '' ) {
		if ( ! isset( $this->cache[ $type ][ $argument ] ) ) {
			$this->cache[ $type ][ $argument ] = \call_user_func( [ $this->meta_surface, "for_$type" ], $argument );
		}

		return $this->cache[ $type ][ $argument ];
	}
}
© 2025 GrazzMean