Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/builders.tar

indexable-builder.php000066600000032301151126400400010630 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use Yoast\WP\SEO\Exceptions\Indexable\Not_Built_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Source_Exception;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;

/**
 * Builder for the indexables.
 *
 * Creates all the indexables.
 */
class Indexable_Builder {

	/**
	 * The author builder.
	 *
	 * @var Indexable_Author_Builder
	 */
	private $author_builder;

	/**
	 * The post builder.
	 *
	 * @var Indexable_Post_Builder
	 */
	private $post_builder;

	/**
	 * The term builder.
	 *
	 * @var Indexable_Term_Builder
	 */
	private $term_builder;

	/**
	 * The home page builder.
	 *
	 * @var Indexable_Home_Page_Builder
	 */
	private $home_page_builder;

	/**
	 * The post type archive builder.
	 *
	 * @var Indexable_Post_Type_Archive_Builder
	 */
	private $post_type_archive_builder;

	/**
	 * The data archive builder.
	 *
	 * @var Indexable_Date_Archive_Builder
	 */
	private $date_archive_builder;

	/**
	 * The system page builder.
	 *
	 * @var Indexable_System_Page_Builder
	 */
	private $system_page_builder;

	/**
	 * The indexable hierarchy builder.
	 *
	 * @var Indexable_Hierarchy_Builder
	 */
	private $hierarchy_builder;

	/**
	 * The primary term builder
	 *
	 * @var Primary_Term_Builder
	 */
	private $primary_term_builder;

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	private $indexable_repository;

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

	/**
	 * The Indexable Version Manager.
	 *
	 * @var Indexable_Version_Manager
	 */
	protected $version_manager;

	/**
	 * Returns the instance of this class constructed through the ORM Wrapper.
	 *
	 * @param Indexable_Author_Builder            $author_builder            The author builder for creating missing indexables.
	 * @param Indexable_Post_Builder              $post_builder              The post builder for creating missing indexables.
	 * @param Indexable_Term_Builder              $term_builder              The term builder for creating missing indexables.
	 * @param Indexable_Home_Page_Builder         $home_page_builder         The front page builder for creating missing indexables.
	 * @param Indexable_Post_Type_Archive_Builder $post_type_archive_builder The post type archive builder for creating missing indexables.
	 * @param Indexable_Date_Archive_Builder      $date_archive_builder      The date archive builder for creating missing indexables.
	 * @param Indexable_System_Page_Builder       $system_page_builder       The search result builder for creating missing indexables.
	 * @param Indexable_Hierarchy_Builder         $hierarchy_builder         The hierarchy builder for creating the indexable hierarchy.
	 * @param Primary_Term_Builder                $primary_term_builder      The primary term builder for creating primary terms for posts.
	 * @param Indexable_Helper                    $indexable_helper          The indexable helper.
	 * @param Indexable_Version_Manager           $version_manager           The indexable version manager.
	 */
	public function __construct(
		Indexable_Author_Builder $author_builder,
		Indexable_Post_Builder $post_builder,
		Indexable_Term_Builder $term_builder,
		Indexable_Home_Page_Builder $home_page_builder,
		Indexable_Post_Type_Archive_Builder $post_type_archive_builder,
		Indexable_Date_Archive_Builder $date_archive_builder,
		Indexable_System_Page_Builder $system_page_builder,
		Indexable_Hierarchy_Builder $hierarchy_builder,
		Primary_Term_Builder $primary_term_builder,
		Indexable_Helper $indexable_helper,
		Indexable_Version_Manager $version_manager
	) {
		$this->author_builder            = $author_builder;
		$this->post_builder              = $post_builder;
		$this->term_builder              = $term_builder;
		$this->home_page_builder         = $home_page_builder;
		$this->post_type_archive_builder = $post_type_archive_builder;
		$this->date_archive_builder      = $date_archive_builder;
		$this->system_page_builder       = $system_page_builder;
		$this->hierarchy_builder         = $hierarchy_builder;
		$this->primary_term_builder      = $primary_term_builder;
		$this->indexable_helper          = $indexable_helper;
		$this->version_manager           = $version_manager;
	}

	/**
	 * Sets the indexable repository. Done to avoid circular dependencies.
	 *
	 * @required
	 *
	 * @param Indexable_Repository $indexable_repository The indexable repository.
	 */
	public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Creates a clean copy of an Indexable to allow for later database operations.
	 *
	 * @param Indexable $indexable The Indexable to copy.
	 *
	 * @return bool|Indexable
	 */
	protected function deep_copy_indexable( $indexable ) {
		return $this->indexable_repository
			->query()
			->create( $indexable->as_array() );
	}

	/**
	 * Creates an indexable by its ID and type.
	 *
	 * @param int            $object_id   The indexable object ID.
	 * @param string         $object_type The indexable object type.
	 * @param Indexable|bool $indexable   Optional. An existing indexable to overwrite.
	 *
	 * @return bool|Indexable Instance of indexable. False when unable to build.
	 */
	public function build_for_id_and_type( $object_id, $object_type, $indexable = false ) {
		$defaults = [
			'object_type' => $object_type,
			'object_id'   => $object_id,
		];

		$indexable = $this->build( $indexable, $defaults );

		return $indexable;
	}

	/**
	 * Creates an indexable for the homepage.
	 *
	 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
	 *
	 * @return Indexable The home page indexable.
	 */
	public function build_for_home_page( $indexable = false ) {
		return $this->build( $indexable, [ 'object_type' => 'home-page' ] );
	}

	/**
	 * Creates an indexable for the date archive.
	 *
	 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
	 *
	 * @return Indexable The date archive indexable.
	 */
	public function build_for_date_archive( $indexable = false ) {
		return $this->build( $indexable, [ 'object_type' => 'date-archive' ] );
	}

	/**
	 * Creates an indexable for a post type archive.
	 *
	 * @param string         $post_type The post type.
	 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
	 *
	 * @return Indexable The post type archive indexable.
	 */
	public function build_for_post_type_archive( $post_type, $indexable = false ) {
		$defaults = [
			'object_type'     => 'post-type-archive',
			'object_sub_type' => $post_type,
		];
		return $this->build( $indexable, $defaults );
	}

	/**
	 * Creates an indexable for a system page.
	 *
	 * @param string         $page_type The type of system page.
	 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
	 *
	 * @return Indexable The search result indexable.
	 */
	public function build_for_system_page( $page_type, $indexable = false ) {
		$defaults = [
			'object_type'     => 'system-page',
			'object_sub_type' => $page_type,
		];
		return $this->build( $indexable, $defaults );
	}

	/**
	 * Ensures we have a valid indexable. Creates one if false is passed.
	 *
	 * @param Indexable|false $indexable The indexable.
	 * @param array           $defaults  The initial properties of the Indexable.
	 *
	 * @return Indexable The indexable.
	 */
	private function ensure_indexable( $indexable, $defaults = [] ) {
		if ( ! $indexable ) {
			return $this->indexable_repository->query()->create( $defaults );
		}

		return $indexable;
	}

	/**
	 * Saves and returns an indexable (on production environments only).
	 *
	 * @param Indexable      $indexable        The indexable.
	 * @param Indexable|null $indexable_before The indexable before possible changes.
	 *
	 * @return Indexable The indexable.
	 */
	protected function save_indexable( $indexable, $indexable_before = null ) {
		$intend_to_save = $this->indexable_helper->should_index_indexables();

		/**
		 * Filter: 'wpseo_should_save_indexable' - Allow developers to enable / disable
		 * saving the indexable when the indexable is updated. Warning: overriding
		 * the intended action may cause problems when moving from a staging to a
		 * production environment because indexable permalinks may get set incorrectly.
		 *
		 * @param Indexable $indexable The indexable to be saved.
		 *
		 * @api bool $intend_to_save True if YoastSEO intends to save the indexable.
		 */
		$intend_to_save = \apply_filters( 'wpseo_should_save_indexable', $intend_to_save, $indexable );

		if ( ! $intend_to_save ) {
			return $indexable;
		}

		// Save the indexable before running the WordPress hook.
		$indexable->save();

		if ( $indexable_before ) {
			/**
			 * Action: 'wpseo_save_indexable' - Allow developers to perform an action
			 * when the indexable is updated.
			 *
			 * @param Indexable $indexable_before The indexable before saving.
			 *
			 * @api Indexable $indexable The saved indexable.
			 */
			\do_action( 'wpseo_save_indexable', $indexable, $indexable_before );
		}

		return $indexable;
	}

	/**
	 * Build and author indexable from an author id if it does not exist yet, or if the author indexable needs to be upgraded.
	 *
	 * @param int $author_id The author id.
	 *
	 * @return Indexable|false The author indexable if it has been built, `false` if it could not be built.
	 */
	protected function maybe_build_author_indexable( $author_id ) {
		$author_indexable = $this->indexable_repository->find_by_id_and_type(
			$author_id,
			'user',
			false
		);
		if ( ! $author_indexable || $this->version_manager->indexable_needs_upgrade( $author_indexable ) ) {
			// Try to build the author.
			$author_defaults  = [
				'object_type' => 'user',
				'object_id'   => $author_id,
			];
			$author_indexable = $this->build( $author_indexable, $author_defaults );
		}
		return $author_indexable;
	}

	// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing -- Exceptions are handled by the catch statement in the method.

	/**
	 * Rebuilds an Indexable from scratch.
	 *
	 * @param Indexable  $indexable The Indexable to (re)build.
	 * @param array|null $defaults  The object type of the Indexable.
	 *
	 * @return Indexable|false The resulting Indexable.
	 */
	public function build( $indexable, $defaults = null ) {
		// Backup the previous Indexable, if there was one.
		$indexable_before = ( $indexable ) ? $this->deep_copy_indexable( $indexable ) : null;

		// Make sure we have an Indexable to work with.
		$indexable = $this->ensure_indexable( $indexable, $defaults );

		try {
			if ( $indexable->object_id === 0 ) {
				throw Not_Built_Exception::invalid_object_id( $indexable->object_id );
			}
			switch ( $indexable->object_type ) {

				case 'post':
					$indexable = $this->post_builder->build( $indexable->object_id, $indexable );
					if ( ! $indexable ) {
						// Indexable for this Post was not built for a reason; e.g. if its post type is excluded.
						return $indexable;
					}

					// Always rebuild the primary term.
					$this->primary_term_builder->build( $indexable->object_id );

					// Always rebuild the hierarchy; this needs the primary term to run correctly.
					$this->hierarchy_builder->build( $indexable );

					// Save indexable, to make sure that it can be queried when determining if an author has public posts.
					$indexable = $this->save_indexable( $indexable, $indexable_before );

					$this->maybe_build_author_indexable( $indexable->author_id );

					break;

				case 'user':
					$indexable = $this->author_builder->build( $indexable->object_id, $indexable );
					break;

				case 'term':
					$indexable = $this->term_builder->build( $indexable->object_id, $indexable );

					$this->hierarchy_builder->build( $indexable );
					break;

				case 'home-page':
					$indexable = $this->home_page_builder->build( $indexable );
					break;

				case 'date-archive':
					$indexable = $this->date_archive_builder->build( $indexable );
					break;

				case 'post-type-archive':
					$indexable = $this->post_type_archive_builder->build( $indexable->object_sub_type, $indexable );
					break;

				case 'system-page':
					$indexable = $this->system_page_builder->build( $indexable->object_sub_type, $indexable );
					break;
			}

			return $this->save_indexable( $indexable, $indexable_before );
		}
		catch ( Source_Exception $exception ) {
			/**
			 * The current indexable could not be indexed. Create a placeholder indexable, so we can
			 * skip this indexable in future indexing runs.
			 *
			 * @var Indexable $indexable
			 */
			$indexable = $this->ensure_indexable(
				$indexable,
				[
					'object_id'   => $indexable->object_id,
					'object_type' => $indexable->object_type,
					'post_status' => 'unindexed',
					'version'     => 0,
				]
			);
			// If we already had an existing indexable, mark it as unindexed. We cannot rely on its validity anymore.
			$indexable->post_status = 'unindexed';
			// Make sure that the indexing process doesn't get stuck in a loop on this broken indexable.
			$indexable = $this->version_manager->set_latest( $indexable );

			return $this->save_indexable( $indexable, $indexable_before );
		}
		catch ( Not_Built_Exception $exception ) {
			return false;
		}
	}

	// phpcs:enable
}
indexable-hierarchy-builder.php000066600000025412151126400400012611 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use WP_Post;
use WP_Term;
use WPSEO_Meta;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Builder for the indexables hierarchy.
 *
 * Builds the indexable hierarchy for indexables.
 */
class Indexable_Hierarchy_Builder {

	/**
	 * Holds a list of indexables where the ancestors are saved for.
	 *
	 * @var array
	 */
	protected $saved_ancestors = [];

	/**
	 * The indexable repository.
	 *
	 * @var Indexable_Repository
	 */
	private $indexable_repository;

	/**
	 * The indexable hierarchy repository.
	 *
	 * @var Indexable_Hierarchy_Repository
	 */
	private $indexable_hierarchy_repository;

	/**
	 * The primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	private $primary_term_repository;

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

	/**
	 * Holds the Post_Helper instance.
	 *
	 * @var Post_Helper
	 */
	private $post;

	/**
	 * Indexable_Author_Builder constructor.
	 *
	 * @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
	 * @param Primary_Term_Repository        $primary_term_repository        The primary term repository.
	 * @param Options_Helper                 $options                        The options helper.
	 * @param Post_Helper                    $post                           The post helper.
	 */
	public function __construct(
		Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
		Primary_Term_Repository $primary_term_repository,
		Options_Helper $options,
		Post_Helper $post
	) {
		$this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
		$this->primary_term_repository        = $primary_term_repository;
		$this->options                        = $options;
		$this->post                           = $post;
	}

	/**
	 * Sets the indexable repository. Done to avoid circular dependencies.
	 *
	 * @required
	 *
	 * @param Indexable_Repository $indexable_repository The indexable repository.
	 */
	public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Builds the ancestor hierarchy for an indexable.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return Indexable The indexable.
	 */
	public function build( Indexable $indexable ) {
		if ( $this->hierarchy_is_built( $indexable ) ) {
			return $indexable;
		}

		$this->indexable_hierarchy_repository->clear_ancestors( $indexable->id );

		$indexable_id = $this->get_indexable_id( $indexable );
		$ancestors    = [];
		if ( $indexable->object_type === 'post' ) {
			$this->add_ancestors_for_post( $indexable_id, $indexable->object_id, $ancestors );
		}

		if ( $indexable->object_type === 'term' ) {
			$this->add_ancestors_for_term( $indexable_id, $indexable->object_id, $ancestors );
		}
		$indexable->ancestors     = \array_reverse( \array_values( $ancestors ) );
		$indexable->has_ancestors = ! empty( $ancestors );
		if ( $indexable->id ) {
			$this->save_ancestors( $indexable );
		}

		return $indexable;
	}

	/**
	 * Checks if a hierarchy is built already for the given indexable.
	 *
	 * @param Indexable $indexable The indexable to check.
	 *
	 * @return bool True when indexable has a built hierarchy.
	 */
	protected function hierarchy_is_built( Indexable $indexable ) {
		if ( \in_array( $indexable->id, $this->saved_ancestors, true ) ) {
			return true;
		}

		$this->saved_ancestors[] = $indexable->id;

		return false;
	}

	/**
	 * Saves the ancestors.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return void
	 */
	private function save_ancestors( $indexable ) {
		if ( empty( $indexable->ancestors ) ) {
			$this->indexable_hierarchy_repository->add_ancestor( $indexable->id, 0, 0 );
			return;
		}
		$depth = \count( $indexable->ancestors );
		foreach ( $indexable->ancestors as $ancestor ) {
			$this->indexable_hierarchy_repository->add_ancestor( $indexable->id, $ancestor->id, $depth );
			--$depth;
		}
	}

	/**
	 * Adds ancestors for a post.
	 *
	 * @param int   $indexable_id The indexable id, this is the id of the original indexable.
	 * @param int   $post_id      The post id, this is the id of the post currently being evaluated.
	 * @param int[] $parents      The indexable IDs of all parents.
	 *
	 * @return void
	 */
	private function add_ancestors_for_post( $indexable_id, $post_id, &$parents ) {
		$post = $this->post->get_post( $post_id );

		if ( ! isset( $post->post_parent ) ) {
			return;
		}

		if ( $post->post_parent !== 0 && $this->post->get_post( $post->post_parent ) !== null ) {
			$ancestor = $this->indexable_repository->find_by_id_and_type( $post->post_parent, 'post' );
			if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
				return;
			}

			$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;

			$this->add_ancestors_for_post( $indexable_id, $ancestor->object_id, $parents );

			return;
		}

		$primary_term_id = $this->find_primary_term_id_for_post( $post );

		if ( $primary_term_id === 0 ) {
			return;
		}

		$ancestor = $this->indexable_repository->find_by_id_and_type( $primary_term_id, 'term' );
		if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
			return;
		}

		$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;

		$this->add_ancestors_for_term( $indexable_id, $ancestor->object_id, $parents );
	}

	/**
	 * Adds ancestors for a term.
	 *
	 * @param int   $indexable_id The indexable id, this is the id of the original indexable.
	 * @param int   $term_id      The term id, this is the id of the term currently being evaluated.
	 * @param int[] $parents      The indexable IDs of all parents.
	 *
	 * @return void
	 */
	private function add_ancestors_for_term( $indexable_id, $term_id, &$parents = [] ) {
		$term         = \get_term( $term_id );
		$term_parents = $this->get_term_parents( $term );

		foreach ( $term_parents as $parent ) {
			$ancestor = $this->indexable_repository->find_by_id_and_type( $parent->term_id, 'term' );
			if ( $this->is_invalid_ancestor( $ancestor, $indexable_id, $parents ) ) {
				continue;
			}

			$parents[ $this->get_indexable_id( $ancestor ) ] = $ancestor;
		}
	}

	/**
	 * Gets the primary term ID for a post.
	 *
	 * @param WP_Post $post The post.
	 *
	 * @return int The primary term ID. 0 if none exists.
	 */
	private function find_primary_term_id_for_post( $post ) {
		$main_taxonomy = $this->options->get( 'post_types-' . $post->post_type . '-maintax' );

		if ( ! $main_taxonomy || $main_taxonomy === '0' ) {
			return 0;
		}

		$primary_term_id = $this->get_primary_term_id( $post->ID, $main_taxonomy );

		if ( $primary_term_id ) {
			$term = \get_term( $primary_term_id );
			if ( $term !== null && ! \is_wp_error( $term ) ) {
				return $primary_term_id;
			}
		}

		$terms = \get_the_terms( $post->ID, $main_taxonomy );

		if ( ! \is_array( $terms ) || empty( $terms ) ) {
			return 0;
		}

		return $this->find_deepest_term_id( $terms );
	}

	/**
	 * Find the deepest term in an array of term objects.
	 *
	 * @param array $terms Terms set.
	 *
	 * @return int The deepest term ID.
	 */
	private function find_deepest_term_id( $terms ) {
		/*
		 * Let's find the deepest term in this array, by looping through and then
		 * unsetting every term that is used as a parent by another one in the array.
		 */
		$terms_by_id = [];
		foreach ( $terms as $term ) {
			$terms_by_id[ $term->term_id ] = $term;
		}
		foreach ( $terms as $term ) {
			unset( $terms_by_id[ $term->parent ] );
		}

		/*
		 * As we could still have two subcategories, from different parent categories,
		 * let's pick the one with the lowest ordered ancestor.
		 */
		$parents_count = -1;
		$term_order    = 9999; // Because ASC.
		$deepest_term  = \reset( $terms_by_id );
		foreach ( $terms_by_id as $term ) {
			$parents = $this->get_term_parents( $term );

			$new_parents_count = \count( $parents );

			if ( $new_parents_count < $parents_count ) {
				continue;
			}

			$parent_order = 9999; // Set default order.
			foreach ( $parents as $parent ) {
				if ( $parent->parent === 0 && isset( $parent->term_order ) ) {
					$parent_order = $parent->term_order;
				}
			}

			// Check if parent has lowest order.
			if ( $new_parents_count > $parents_count || $parent_order < $term_order ) {
				$term_order   = $parent_order;
				$deepest_term = $term;
			}

			$parents_count = $new_parents_count;
		}

		return $deepest_term->term_id;
	}

	/**
	 * Get a term's parents.
	 *
	 * @param WP_Term $term Term to get the parents for.
	 *
	 * @return WP_Term[] An array of all this term's parents.
	 */
	private function get_term_parents( $term ) {
		$tax     = $term->taxonomy;
		$parents = [];
		while ( (int) $term->parent !== 0 ) {
			$term      = \get_term( $term->parent, $tax );
			$parents[] = $term;
		}

		return $parents;
	}

	/**
	 * Checks if an ancestor is valid to add.
	 *
	 * @param Indexable $ancestor     The ancestor (presumed indexable) to check.
	 * @param int       $indexable_id The indexable id we're adding ancestors for.
	 * @param int[]     $parents      The indexable ids of the parents already added.
	 *
	 * @return bool
	 */
	private function is_invalid_ancestor( $ancestor, $indexable_id, $parents ) {
		// If the ancestor is not an Indexable, it is invalid by default.
		if ( ! \is_a( $ancestor, 'Yoast\WP\SEO\Models\Indexable' ) ) {
			return true;
		}

		// Don't add ancestors if they're unindexed, already added or the same as the main object.
		if ( $ancestor->post_status === 'unindexed' ) {
			return true;
		}

		$ancestor_id = $this->get_indexable_id( $ancestor );
		if ( \array_key_exists( $ancestor_id, $parents ) ) {
			return true;
		}

		if ( $ancestor_id === $indexable_id ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the ID for an indexable. Catches situations where the id is null due to errors.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return string|int A unique ID for the indexable.
	 */
	private function get_indexable_id( Indexable $indexable ) {
		if ( $indexable->id === 0 ) {
			return "{$indexable->object_type}:{$indexable->object_id}";
		}

		return $indexable->id;
	}

	/**
	 * Returns the primary term id of a post.
	 *
	 * @param int    $post_id       The post ID.
	 * @param string $main_taxonomy The main taxonomy.
	 *
	 * @return int The ID of the primary term.
	 */
	private function get_primary_term_id( $post_id, $main_taxonomy ) {
		$primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, $main_taxonomy, false );

		if ( $primary_term ) {
			return $primary_term->term_id;
		}

		return \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_' . $main_taxonomy, true );
	}
}
indexable-term-builder.php000066600000020026151126400400011576 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use wpdb;
use Yoast\WP\SEO\Exceptions\Indexable\Invalid_Term_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Found_Exception;
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Built_Exception;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Term Builder for the indexables.
 *
 * Formats the term meta to indexable format.
 */
class Indexable_Term_Builder {

	use Indexable_Social_Image_Trait;

	/**
	 * Holds the taxonomy helper instance.
	 *
	 * @var Taxonomy_Helper
	 */
	protected $taxonomy_helper;

	/**
	 * The latest version of the Indexable_Term_Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Holds the taxonomy helper instance.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

	/**
	 * The WPDB instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Indexable_Term_Builder constructor.
	 *
	 * @param Taxonomy_Helper            $taxonomy_helper The taxonomy helper.
	 * @param Indexable_Builder_Versions $versions        The latest version of each Indexable Builder.
	 * @param Post_Helper                $post_helper     The post helper.
	 * @param wpdb                       $wpdb            The WPDB instance.
	 */
	public function __construct(
		Taxonomy_Helper $taxonomy_helper,
		Indexable_Builder_Versions $versions,
		Post_Helper $post_helper,
		wpdb $wpdb
	) {
		$this->taxonomy_helper = $taxonomy_helper;
		$this->version         = $versions->get_latest_version_for_type( 'term' );
		$this->post_helper     = $post_helper;
		$this->wpdb            = $wpdb;
	}

	/**
	 * Formats the data.
	 *
	 * @param int       $term_id   ID of the term to save data for.
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return bool|Indexable The extended indexable. False when unable to build.
	 *
	 * @throws Invalid_Term_Exception   When the term is invalid.
	 * @throws Term_Not_Built_Exception When the term is not viewable.
	 * @throws Term_Not_Found_Exception When the term is not found.
	 */
	public function build( $term_id, $indexable ) {
		$term = \get_term( $term_id );

		if ( $term === null ) {
			throw new Term_Not_Found_Exception();
		}

		if ( \is_wp_error( $term ) ) {
			throw new Invalid_Term_Exception( $term->get_error_message() );
		}

		$indexable_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
		if ( ! \in_array( $term->taxonomy, $indexable_taxonomies, true ) ) {
			throw Term_Not_Built_Exception::because_not_indexable( $term_id );
		}

		$term_link = \get_term_link( $term, $term->taxonomy );

		if ( \is_wp_error( $term_link ) ) {
			throw new Invalid_Term_Exception( $term_link->get_error_message() );
		}

		$term_meta = $this->taxonomy_helper->get_term_meta( $term );

		$indexable->object_id       = $term_id;
		$indexable->object_type     = 'term';
		$indexable->object_sub_type = $term->taxonomy;
		$indexable->permalink       = $term_link;
		$indexable->blog_id         = \get_current_blog_id();

		$indexable->primary_focus_keyword_score = $this->get_keyword_score(
			$this->get_meta_value( 'wpseo_focuskw', $term_meta ),
			$this->get_meta_value( 'wpseo_linkdex', $term_meta )
		);

		$indexable->is_robots_noindex = $this->get_noindex_value( $this->get_meta_value( 'wpseo_noindex', $term_meta ) );
		$indexable->is_public         = ( $indexable->is_robots_noindex === null ) ? null : ! $indexable->is_robots_noindex;

		$this->reset_social_images( $indexable );

		foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) {
			$indexable->{$indexable_key} = $this->get_meta_value( $meta_key, $term_meta );
		}

		if ( empty( $indexable->breadcrumb_title ) ) {
			$indexable->breadcrumb_title = $term->name;
		}

		$this->handle_social_images( $indexable );

		$indexable->is_cornerstone = $this->get_meta_value( 'wpseo_is_cornerstone', $term_meta );

		// Not implemented yet.
		$indexable->is_robots_nofollow     = null;
		$indexable->is_robots_noarchive    = null;
		$indexable->is_robots_noimageindex = null;
		$indexable->is_robots_nosnippet    = null;

		$timestamps                      = $this->get_object_timestamps( $term_id, $term->taxonomy );
		$indexable->object_published_at  = $timestamps->published_at;
		$indexable->object_last_modified = $timestamps->last_modified;

		$indexable->version = $this->version;

		return $indexable;
	}

	/**
	 * Converts the meta noindex value to the indexable value.
	 *
	 * @param string $meta_value Term meta to base the value on.
	 *
	 * @return bool|null
	 */
	protected function get_noindex_value( $meta_value ) {
		if ( $meta_value === 'noindex' ) {
			return true;
		}

		if ( $meta_value === 'index' ) {
			return false;
		}

		return null;
	}

	/**
	 * Determines the focus keyword score.
	 *
	 * @param string $keyword The focus keyword that is set.
	 * @param int    $score   The score saved on the meta data.
	 *
	 * @return int|null Score to use.
	 */
	protected function get_keyword_score( $keyword, $score ) {
		if ( empty( $keyword ) ) {
			return null;
		}

		return $score;
	}

	/**
	 * Retrieves the lookup table.
	 *
	 * @return array Lookup table for the indexable fields.
	 */
	protected function get_indexable_lookup() {
		return [
			'wpseo_canonical'             => 'canonical',
			'wpseo_focuskw'               => 'primary_focus_keyword',
			'wpseo_title'                 => 'title',
			'wpseo_desc'                  => 'description',
			'wpseo_content_score'         => 'readability_score',
			'wpseo_bctitle'               => 'breadcrumb_title',
			'wpseo_opengraph-title'       => 'open_graph_title',
			'wpseo_opengraph-description' => 'open_graph_description',
			'wpseo_opengraph-image'       => 'open_graph_image',
			'wpseo_opengraph-image-id'    => 'open_graph_image_id',
			'wpseo_twitter-title'         => 'twitter_title',
			'wpseo_twitter-description'   => 'twitter_description',
			'wpseo_twitter-image'         => 'twitter_image',
			'wpseo_twitter-image-id'      => 'twitter_image_id',
		];
	}

	/**
	 * Retrieves a meta value from the given meta data.
	 *
	 * @param string $meta_key  The key to extract.
	 * @param array  $term_meta The meta data.
	 *
	 * @return string|null The meta value.
	 */
	protected function get_meta_value( $meta_key, $term_meta ) {
		if ( ! $term_meta || ! \array_key_exists( $meta_key, $term_meta ) ) {
			return null;
		}

		$value = $term_meta[ $meta_key ];
		if ( \is_string( $value ) && $value === '' ) {
			return null;
		}

		return $value;
	}

	/**
	 * Finds an alternative image for the social image.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return array|bool False when not found, array with data when found.
	 */
	protected function find_alternative_image( Indexable $indexable ) {
		$content_image = $this->image->get_term_content_image( $indexable->object_id );
		if ( $content_image ) {
			return [
				'image'  => $content_image,
				'source' => 'first-content-image',
			];
		}

		return false;
	}

	/**
	 * Returns the timestamps for a given term.
	 *
	 * @param int    $term_id  The term ID.
	 * @param string $taxonomy The taxonomy.
	 *
	 * @return object An object with last_modified and published_at timestamps.
	 */
	protected function get_object_timestamps( $term_id, $taxonomy ) {
		$post_statuses = $this->post_helper->get_public_post_statuses();

		$sql = "
			SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
			FROM	{$this->wpdb->posts} AS p
			INNER JOIN {$this->wpdb->term_relationships} AS term_rel
				ON		term_rel.object_id = p.ID
			INNER JOIN {$this->wpdb->term_taxonomy} AS term_tax
				ON		term_tax.term_taxonomy_id = term_rel.term_taxonomy_id
				AND		term_tax.taxonomy = %s
				AND		term_tax.term_id = %d
			WHERE	p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
				AND		p.post_password = ''
		";

		$replacements = \array_merge( [ $taxonomy, $term_id ], $post_statuses );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
		return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
	}
}
primary-term-builder.php000066600000004325151126400400011332 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use Yoast\WP\SEO\Helpers\Meta_Helper;
use Yoast\WP\SEO\Helpers\Primary_Term_Helper;
use Yoast\WP\SEO\Repositories\Primary_Term_Repository;

/**
 * Primary term builder.
 *
 * Creates the primary term for a post.
 */
class Primary_Term_Builder {

	/**
	 * The primary term repository.
	 *
	 * @var Primary_Term_Repository
	 */
	protected $repository;

	/**
	 * The primary term helper.
	 *
	 * @var Primary_Term_Helper
	 */
	private $primary_term;

	/**
	 * The meta helper.
	 *
	 * @var Meta_Helper
	 */
	private $meta;

	/**
	 * Primary_Term_Builder constructor.
	 *
	 * @param Primary_Term_Repository $repository   The primary term repository.
	 * @param Primary_Term_Helper     $primary_term The primary term helper.
	 * @param Meta_Helper             $meta         The meta helper.
	 */
	public function __construct(
		Primary_Term_Repository $repository,
		Primary_Term_Helper $primary_term,
		Meta_Helper $meta
	) {
		$this->repository   = $repository;
		$this->primary_term = $primary_term;
		$this->meta         = $meta;
	}

	/**
	 * Formats and saves the primary terms for the post with the given post id.
	 *
	 * @param int $post_id The post ID.
	 *
	 * @return void
	 */
	public function build( $post_id ) {
		foreach ( $this->primary_term->get_primary_term_taxonomies( $post_id ) as $taxonomy ) {
			$this->save_primary_term( $post_id, $taxonomy->name );
		}
	}

	/**
	 * Save the primary term for a specific taxonomy.
	 *
	 * @param int    $post_id  Post ID to save primary term for.
	 * @param string $taxonomy Taxonomy to save primary term for.
	 *
	 * @return void
	 */
	protected function save_primary_term( $post_id, $taxonomy ) {
		$term_id = $this->meta->get_value( 'primary_' . $taxonomy, $post_id );

		$term_selected = ! empty( $term_id );
		$primary_term  = $this->repository->find_by_post_id_and_taxonomy( $post_id, $taxonomy, $term_selected );

		// Removes the indexable when no term found.
		if ( ! $term_selected ) {
			if ( $primary_term ) {
				$primary_term->delete();
			}

			return;
		}

		$primary_term->term_id  = $term_id;
		$primary_term->post_id  = $post_id;
		$primary_term->taxonomy = $taxonomy;
		$primary_term->blog_id  = \get_current_blog_id();
		$primary_term->save();
	}
}
indexable-author-builder.php000066600000015624151126400400012141 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use wpdb;
use Yoast\WP\SEO\Exceptions\Indexable\Author_Not_Built_Exception;
use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Author Builder for the indexables.
 *
 * Formats the author meta to indexable format.
 */
class Indexable_Author_Builder {

	use Indexable_Social_Image_Trait;

	/**
	 * The author archive helper.
	 *
	 * @var Author_Archive_Helper
	 */
	private $author_archive;

	/**
	 * The latest version of the Indexable_Author_Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Holds the taxonomy helper instance.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

	/**
	 * The WPDB instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Indexable_Author_Builder constructor.
	 *
	 * @param Author_Archive_Helper      $author_archive The author archive helper.
	 * @param Indexable_Builder_Versions $versions       The Indexable version manager.
	 * @param Post_Helper                $post_helper    The post helper.
	 * @param wpdb                       $wpdb           The WPDB instance.
	 */
	public function __construct(
		Author_Archive_Helper $author_archive,
		Indexable_Builder_Versions $versions,
		Post_Helper $post_helper,
		wpdb $wpdb
	) {
		$this->author_archive = $author_archive;
		$this->version        = $versions->get_latest_version_for_type( 'user' );
		$this->post_helper    = $post_helper;
		$this->wpdb           = $wpdb;
	}

	/**
	 * Formats the data.
	 *
	 * @param int       $user_id   The user to retrieve the indexable for.
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return Indexable The extended indexable.
	 *
	 * @throws Author_Not_Built_Exception When author is not built.
	 */
	public function build( $user_id, Indexable $indexable ) {
		$exception = $this->check_if_user_should_be_indexed( $user_id );
		if ( $exception ) {
			throw $exception;
		}

		$meta_data = $this->get_meta_data( $user_id );

		$indexable->object_id              = $user_id;
		$indexable->object_type            = 'user';
		$indexable->permalink              = \get_author_posts_url( $user_id );
		$indexable->title                  = $meta_data['wpseo_title'];
		$indexable->description            = $meta_data['wpseo_metadesc'];
		$indexable->is_cornerstone         = false;
		$indexable->is_robots_noindex      = ( $meta_data['wpseo_noindex_author'] === 'on' );
		$indexable->is_robots_nofollow     = null;
		$indexable->is_robots_noarchive    = null;
		$indexable->is_robots_noimageindex = null;
		$indexable->is_robots_nosnippet    = null;
		$indexable->is_public              = ( $indexable->is_robots_noindex ) ? false : null;
		$indexable->has_public_posts       = $this->author_archive->author_has_public_posts( $user_id );
		$indexable->blog_id                = \get_current_blog_id();

		$this->reset_social_images( $indexable );
		$this->handle_social_images( $indexable );

		$timestamps                      = $this->get_object_timestamps( $user_id );
		$indexable->object_published_at  = $timestamps->published_at;
		$indexable->object_last_modified = $timestamps->last_modified;

		$indexable->version = $this->version;

		return $indexable;
	}

	/**
	 * Retrieves the meta data for this indexable.
	 *
	 * @param int $user_id The user to retrieve the meta data for.
	 *
	 * @return array List of meta entries.
	 */
	protected function get_meta_data( $user_id ) {
		$keys = [
			'wpseo_title',
			'wpseo_metadesc',
			'wpseo_noindex_author',
		];

		$output = [];
		foreach ( $keys as $key ) {
			$output[ $key ] = $this->get_author_meta( $user_id, $key );
		}

		return $output;
	}

	/**
	 * Retrieves the author meta.
	 *
	 * @param int    $user_id The user to retrieve the indexable for.
	 * @param string $key     The meta entry to retrieve.
	 *
	 * @return string|null The value of the meta field.
	 */
	protected function get_author_meta( $user_id, $key ) {
		$value = \get_the_author_meta( $key, $user_id );
		if ( \is_string( $value ) && $value === '' ) {
			return null;
		}

		return $value;
	}

	/**
	 * Finds an alternative image for the social image.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return array|bool False when not found, array with data when found.
	 */
	protected function find_alternative_image( Indexable $indexable ) {
		$gravatar_image = \get_avatar_url(
			$indexable->object_id,
			[
				'size'   => 500,
				'scheme' => 'https',
			]
		);
		if ( $gravatar_image ) {
			return [
				'image'  => $gravatar_image,
				'source' => 'gravatar-image',
			];
		}

		return false;
	}

	/**
	 * Returns the timestamps for a given author.
	 *
	 * @param int $author_id The author ID.
	 *
	 * @return object An object with last_modified and published_at timestamps.
	 */
	protected function get_object_timestamps( $author_id ) {
		$post_statuses = $this->post_helper->get_public_post_statuses();

		$sql = "
			SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
			FROM {$this->wpdb->posts} AS p
			WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
				AND p.post_password = ''
				AND p.post_author = %d
		";

		$replacements = \array_merge( $post_statuses, [ $author_id ] );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
		return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
	}

	/**
	 * Checks if the user should be indexed.
	 * Returns an exception with an appropriate message if not.
	 *
	 * @param string $user_id The user id.
	 *
	 * @return Author_Not_Built_Exception|null The exception if it should not be indexed, or `null` if it should.
	 */
	protected function check_if_user_should_be_indexed( $user_id ) {
		$exception = null;

		if ( $this->author_archive->are_disabled() ) {
			$exception = Author_Not_Built_Exception::author_archives_are_disabled( $user_id );
		}

		// We will check if the author has public posts the WP way, instead of the indexable way, to make sure we get proper results even if SEO optimization is not run.
		if ( $this->author_archive->author_has_public_posts_wp( $user_id ) === false ) {
			$exception = Author_Not_Built_Exception::author_archives_are_not_indexed_for_users_without_posts( $user_id );
		}

		/**
		 * Filter: Include or exclude a user from being build and saved as an indexable.
		 * Return an `Author_Not_Built_Exception` when the indexable should not be build, with an appropriate message telling why it should not be built.
		 * Return `null` if the indexable should be build.
		 *
		 * @param Author_Not_Built_Exception|null $exception An exception if the indexable is not being built, `null` if the indexable should be built.
		 * @param string                          $user_id   The ID of the user that should or should not be excluded.
		 */
		return \apply_filters( 'wpseo_should_build_and_save_user_indexable', $exception, $user_id );
	}
}
indexable-post-builder.php000066600000030102151126400400011610 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use WP_Error;
use WP_Post;
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Found_Exception;
use Yoast\WP\SEO\Helpers\Meta_Helper;
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;
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Built_Exception;

/**
 * Post Builder for the indexables.
 *
 * Formats the post meta to indexable format.
 */
class Indexable_Post_Builder {

	use Indexable_Social_Image_Trait;

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

	/**
	 * Holds the Post_Helper instance.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

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

	/**
	 * Knows the latest version of the Indexable post builder type.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * The meta helper.
	 *
	 * @var Meta_Helper
	 */
	protected $meta;

	/**
	 * Indexable_Post_Builder constructor.
	 *
	 * @param Post_Helper                $post_helper      The post helper.
	 * @param Post_Type_Helper           $post_type_helper The post type helper.
	 * @param Indexable_Builder_Versions $versions         The indexable builder versions.
	 * @param Meta_Helper                $meta             The meta helper.
	 */
	public function __construct(
		Post_Helper $post_helper,
		Post_Type_Helper $post_type_helper,
		Indexable_Builder_Versions $versions,
		Meta_Helper $meta
	) {
		$this->post_helper      = $post_helper;
		$this->post_type_helper = $post_type_helper;
		$this->version          = $versions->get_latest_version_for_type( 'post' );
		$this->meta             = $meta;
	}

	/**
	 * Sets the indexable repository. Done to avoid circular dependencies.
	 *
	 * @required
	 *
	 * @param Indexable_Repository $indexable_repository The indexable repository.
	 */
	public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
		$this->indexable_repository = $indexable_repository;
	}

	/**
	 * Formats the data.
	 *
	 * @param int       $post_id   The post ID to use.
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return bool|Indexable The extended indexable. False when unable to build.
	 *
	 * @throws Post_Not_Found_Exception When the post could not be found.
	 * @throws Post_Not_Built_Exception When the post should not be indexed.
	 */
	public function build( $post_id, $indexable ) {
		if ( ! $this->post_helper->is_post_indexable( $post_id ) ) {
			throw Post_Not_Built_Exception::because_not_indexable( $post_id );
		}

		$post = $this->post_helper->get_post( $post_id );

		if ( $post === null ) {
			throw new Post_Not_Found_Exception();
		}

		if ( $this->should_exclude_post( $post ) ) {
			throw Post_Not_Built_Exception::because_post_type_excluded( $post_id );
		}

		$indexable->object_id       = $post_id;
		$indexable->object_type     = 'post';
		$indexable->object_sub_type = $post->post_type;
		$indexable->permalink       = $this->get_permalink( $post->post_type, $post_id );

		$indexable->primary_focus_keyword_score = $this->get_keyword_score(
			$this->meta->get_value( 'focuskw', $post_id ),
			(int) $this->meta->get_value( 'linkdex', $post_id )
		);

		$indexable->readability_score = (int) $this->meta->get_value( 'content_score', $post_id );

		$indexable->is_cornerstone    = ( $this->meta->get_value( 'is_cornerstone', $post_id ) === '1' );
		$indexable->is_robots_noindex = $this->get_robots_noindex(
			(int) $this->meta->get_value( 'meta-robots-noindex', $post_id )
		);

		// Set additional meta-robots values.
		$indexable->is_robots_nofollow = ( $this->meta->get_value( 'meta-robots-nofollow', $post_id ) === '1' );
		$noindex_advanced              = $this->meta->get_value( 'meta-robots-adv', $post_id );
		$meta_robots                   = \explode( ',', $noindex_advanced );

		foreach ( $this->get_robots_options() as $meta_robots_option ) {
			$indexable->{'is_robots_' . $meta_robots_option} = \in_array( $meta_robots_option, $meta_robots, true ) ? 1 : null;
		}

		$this->reset_social_images( $indexable );

		foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) {
			$indexable->{$indexable_key} = $this->empty_string_to_null( $this->meta->get_value( $meta_key, $post_id ) );
		}

		if ( empty( $indexable->breadcrumb_title ) ) {
			$indexable->breadcrumb_title = \wp_strip_all_tags( \get_the_title( $post_id ), true );
		}

		$this->handle_social_images( $indexable );

		$indexable->author_id   = $post->post_author;
		$indexable->post_parent = $post->post_parent;

		$indexable->number_of_pages  = $this->get_number_of_pages_for_post( $post );
		$indexable->post_status      = $post->post_status;
		$indexable->is_protected     = $post->post_password !== '';
		$indexable->is_public        = $this->is_public( $indexable );
		$indexable->has_public_posts = $this->has_public_posts( $indexable );
		$indexable->blog_id          = \get_current_blog_id();

		$indexable->schema_page_type    = $this->empty_string_to_null( $this->meta->get_value( 'schema_page_type', $post_id ) );
		$indexable->schema_article_type = $this->empty_string_to_null( $this->meta->get_value( 'schema_article_type', $post_id ) );

		$indexable->object_last_modified = $post->post_modified_gmt;
		$indexable->object_published_at  = $post->post_date_gmt;

		$indexable->version = $this->version;

		return $indexable;
	}

	/**
	 * Retrieves the permalink for a post with the given post type and ID.
	 *
	 * @param string $post_type The post type.
	 * @param int    $post_id   The post ID.
	 *
	 * @return false|string|WP_Error The permalink.
	 */
	protected function get_permalink( $post_type, $post_id ) {
		if ( $post_type !== 'attachment' ) {
			return \get_permalink( $post_id );
		}

		return \wp_get_attachment_url( $post_id );
	}

	/**
	 * Determines the value of is_public.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return bool|null Whether or not the post type is public. Null if no override is set.
	 */
	protected function is_public( $indexable ) {
		if ( $indexable->is_protected === true ) {
			return false;
		}

		if ( $indexable->is_robots_noindex === true ) {
			return false;
		}

		// Attachments behave differently than the other post types, since they inherit from their parent.
		if ( $indexable->object_sub_type === 'attachment' ) {
			return $this->is_public_attachment( $indexable );
		}

		if ( ! \in_array( $indexable->post_status, $this->post_helper->get_public_post_statuses(), true ) ) {
			return false;
		}

		if ( $indexable->is_robots_noindex === false ) {
			return true;
		}

		return null;
	}

	/**
	 * Determines the value of is_public for attachments.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return bool|null False when it has no parent. Null when it has a parent.
	 */
	protected function is_public_attachment( $indexable ) {
		// If the attachment has no parent, it should not be public.
		if ( empty( $indexable->post_parent ) ) {
			return false;
		}

		// If the attachment has a parent, the is_public should be NULL.
		return null;
	}

	/**
	 * Determines the value of has_public_posts.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return bool|null Whether the attachment has a public parent, can be true, false and null. Null when it is not an attachment.
	 */
	protected function has_public_posts( $indexable ) {
		// Only attachments (and authors) have this value.
		if ( $indexable->object_sub_type !== 'attachment' ) {
			return null;
		}

		// The attachment should have a post parent.
		if ( empty( $indexable->post_parent ) ) {
			return false;
		}

		// The attachment should inherit the post status.
		if ( $indexable->post_status !== 'inherit' ) {
			return false;
		}

		// The post parent should be public.
		$post_parent_indexable = $this->indexable_repository->find_by_id_and_type( $indexable->post_parent, 'post' );
		if ( $post_parent_indexable !== false ) {
			return $post_parent_indexable->is_public;
		}

		return false;
	}

	/**
	 * Converts the meta robots noindex value to the indexable value.
	 *
	 * @param int $value Meta value to convert.
	 *
	 * @return bool|null True for noindex, false for index, null for default of parent/type.
	 */
	protected function get_robots_noindex( $value ) {
		$value = (int) $value;

		switch ( $value ) {
			case 1:
				return true;
			case 2:
				return false;
		}

		return null;
	}

	/**
	 * Retrieves the robot options to search for.
	 *
	 * @return array List of robots values.
	 */
	protected function get_robots_options() {
		return [ 'noimageindex', 'noarchive', 'nosnippet' ];
	}

	/**
	 * Determines the focus keyword score.
	 *
	 * @param string $keyword The focus keyword that is set.
	 * @param int    $score   The score saved on the meta data.
	 *
	 * @return int|null Score to use.
	 */
	protected function get_keyword_score( $keyword, $score ) {
		if ( empty( $keyword ) ) {
			return null;
		}

		return $score;
	}

	/**
	 * Retrieves the lookup table.
	 *
	 * @return array Lookup table for the indexable fields.
	 */
	protected function get_indexable_lookup() {
		return [
			'focuskw'                        => 'primary_focus_keyword',
			'canonical'                      => 'canonical',
			'title'                          => 'title',
			'metadesc'                       => 'description',
			'bctitle'                        => 'breadcrumb_title',
			'opengraph-title'                => 'open_graph_title',
			'opengraph-image'                => 'open_graph_image',
			'opengraph-image-id'             => 'open_graph_image_id',
			'opengraph-description'          => 'open_graph_description',
			'twitter-title'                  => 'twitter_title',
			'twitter-image'                  => 'twitter_image',
			'twitter-image-id'               => 'twitter_image_id',
			'twitter-description'            => 'twitter_description',
			'estimated-reading-time-minutes' => 'estimated_reading_time_minutes',
		];
	}

	/**
	 * Finds an alternative image for the social image.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return array|bool False when not found, array with data when found.
	 */
	protected function find_alternative_image( Indexable $indexable ) {
		if (
			$indexable->object_sub_type === 'attachment'
			&& $this->image->is_valid_attachment( $indexable->object_id )
		) {
			return [
				'image_id' => $indexable->object_id,
				'source'   => 'attachment-image',
			];
		}

		$featured_image_id = $this->image->get_featured_image_id( $indexable->object_id );
		if ( $featured_image_id ) {
			return [
				'image_id' => $featured_image_id,
				'source'   => 'featured-image',
			];
		}

		$gallery_image = $this->image->get_gallery_image( $indexable->object_id );
		if ( $gallery_image ) {
			return [
				'image'  => $gallery_image,
				'source' => 'gallery-image',
			];
		}

		$content_image = $this->image->get_post_content_image( $indexable->object_id );
		if ( $content_image ) {
			return [
				'image'  => $content_image,
				'source' => 'first-content-image',
			];
		}

		return false;
	}

	/**
	 * Gets the number of pages for a post.
	 *
	 * @param object $post The post object.
	 *
	 * @return int|null The number of pages or null if the post isn't paginated.
	 */
	protected function get_number_of_pages_for_post( $post ) {
		$number_of_pages = ( \substr_count( $post->post_content, '<!--nextpage-->' ) + 1 );

		if ( $number_of_pages <= 1 ) {
			return null;
		}

		return $number_of_pages;
	}

	/**
	 * Checks whether an indexable should be built for this post.
	 *
	 * @param WP_Post $post The post for which an indexable should be built.
	 *
	 * @return bool `true` if the post should be excluded from building, `false` if not.
	 */
	protected function should_exclude_post( $post ) {
		return $this->post_type_helper->is_excluded( $post->post_type );
	}

	/**
	 * Transforms an empty string into null. Leaves non-empty strings intact.
	 *
	 * @param string $text The string.
	 *
	 * @return string|null The input string or null.
	 */
	protected function empty_string_to_null( $text ) {
		if ( ! \is_string( $text ) || $text === '' ) {
			return null;
		}

		return $text;
	}
}
indexable-home-page-builder.php000066600000010131151126400400012465 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use wpdb;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Homepage Builder for the indexables.
 *
 * Formats the homepage meta to indexable format.
 */
class Indexable_Home_Page_Builder {

	use Indexable_Social_Image_Trait;

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

	/**
	 * The URL helper.
	 *
	 * @var Url_Helper
	 */
	protected $url_helper;

	/**
	 * The latest version of the Indexable-Home-Page-Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Holds the taxonomy helper instance.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

	/**
	 * The WPDB instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Indexable_Home_Page_Builder constructor.
	 *
	 * @param Options_Helper             $options     The options helper.
	 * @param Url_Helper                 $url_helper  The url helper.
	 * @param Indexable_Builder_Versions $versions    Knows the latest version of each Indexable type.
	 * @param Post_Helper                $post_helper The post helper.
	 * @param wpdb                       $wpdb        The WPDB instance.
	 */
	public function __construct(
		Options_Helper $options,
		Url_Helper $url_helper,
		Indexable_Builder_Versions $versions,
		Post_Helper $post_helper,
		wpdb $wpdb
	) {
		$this->options     = $options;
		$this->url_helper  = $url_helper;
		$this->version     = $versions->get_latest_version_for_type( 'home-page' );
		$this->post_helper = $post_helper;
		$this->wpdb        = $wpdb;
	}

	/**
	 * Formats the data.
	 *
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return Indexable The extended indexable.
	 */
	public function build( $indexable ) {
		$indexable->object_type      = 'home-page';
		$indexable->title            = $this->options->get( 'title-home-wpseo' );
		$indexable->breadcrumb_title = $this->options->get( 'breadcrumbs-home' );
		$indexable->permalink        = $this->url_helper->home();
		$indexable->blog_id          = \get_current_blog_id();
		$indexable->description      = $this->options->get( 'metadesc-home-wpseo' );
		if ( empty( $indexable->description ) ) {
			$indexable->description = \get_bloginfo( 'description' );
		}

		$indexable->is_robots_noindex = \get_option( 'blog_public' ) === '0';

		$indexable->open_graph_title       = $this->options->get( 'open_graph_frontpage_title' );
		$indexable->open_graph_image       = $this->options->get( 'open_graph_frontpage_image' );
		$indexable->open_graph_image_id    = $this->options->get( 'open_graph_frontpage_image_id' );
		$indexable->open_graph_description = $this->options->get( 'open_graph_frontpage_desc' );

		// Reset the OG image source & meta.
		$indexable->open_graph_image_source = null;
		$indexable->open_graph_image_meta   = null;

		// When the image or image id is set.
		if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) {
			$indexable->open_graph_image_source = 'set-by-user';

			$this->set_open_graph_image_meta_data( $indexable );
		}

		$timestamps                      = $this->get_object_timestamps();
		$indexable->object_published_at  = $timestamps->published_at;
		$indexable->object_last_modified = $timestamps->last_modified;

		$indexable->version = $this->version;

		return $indexable;
	}

	/**
	 * Returns the timestamps for the homepage.
	 *
	 * @return object An object with last_modified and published_at timestamps.
	 */
	protected function get_object_timestamps() {
		$post_statuses = $this->post_helper->get_public_post_statuses();

		$sql = "
			SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
			FROM {$this->wpdb->posts} AS p
			WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
				AND p.post_password = ''
				AND p.post_type = 'post'
		";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
		return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $post_statuses ) );
	}
}
.htaccess000066600000000424151126400400006337 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>indexable-link-builder.php000066600000033043151126400400011567 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Models\SEO_Links;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\SEO_Links_Repository;

/**
 * Indexable link builder.
 */
class Indexable_Link_Builder {

	/**
	 * The SEO links repository.
	 *
	 * @var SEO_Links_Repository
	 */
	protected $seo_links_repository;

	/**
	 * The url helper.
	 *
	 * @var Url_Helper
	 */
	protected $url_helper;

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

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

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

	/**
	 * Indexable_Link_Builder constructor.
	 *
	 * @param SEO_Links_Repository $seo_links_repository The SEO links repository.
	 * @param Url_Helper           $url_helper           The URL helper.
	 * @param Post_Helper          $post_helper          The post helper.
	 */
	public function __construct(
		SEO_Links_Repository $seo_links_repository,
		Url_Helper $url_helper,
		Post_Helper $post_helper
	) {
		$this->seo_links_repository = $seo_links_repository;
		$this->url_helper           = $url_helper;
		$this->post_helper          = $post_helper;
	}

	/**
	 * Sets the indexable repository.
	 *
	 * @required
	 *
	 * @param Indexable_Repository $indexable_repository The indexable repository.
	 * @param Image_Helper         $image_helper         The image helper.
	 *
	 * @return void
	 */
	public function set_dependencies(
		Indexable_Repository $indexable_repository,
		Image_Helper $image_helper
	) {
		$this->indexable_repository = $indexable_repository;
		$this->image_helper         = $image_helper;
	}

	/**
	 * Builds the links for a post.
	 *
	 * @param Indexable $indexable The indexable.
	 * @param string    $content   The content. Expected to be unfiltered.
	 *
	 * @return SEO_Links[] The created SEO links.
	 */
	public function build( $indexable, $content ) {
		global $post;
		if ( $indexable->object_type === 'post' ) {
			$post_backup = $post;
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
			$post = $this->post_helper->get_post( $indexable->object_id );
			\setup_postdata( $post );
			$content = \apply_filters( 'the_content', $content );
			\wp_reset_postdata();
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- To setup the post we need to do this explicitly.
			$post = $post_backup;
		}

		$content = \str_replace( ']]>', ']]&gt;', $content );
		$links   = $this->gather_links( $content );
		$images  = $this->gather_images( $content );

		if ( empty( $links ) && empty( $images ) ) {
			$indexable->link_count = 0;
			$this->update_related_indexables( $indexable, [] );

			return [];
		}

		$links = $this->create_links( $indexable, $links, $images );

		$this->update_related_indexables( $indexable, $links );

		$indexable->link_count = $this->get_internal_link_count( $links );

		return $links;
	}

	/**
	 * Deletes all SEO links for an indexable.
	 *
	 * @param Indexable $indexable The indexable.
	 *
	 * @return void
	 */
	public function delete( $indexable ) {
		$links = ( $this->seo_links_repository->find_all_by_indexable_id( $indexable->id ) );
		$this->seo_links_repository->delete_all_by_indexable_id( $indexable->id );

		$linked_indexable_ids = [];
		foreach ( $links as $link ) {
			$linked_indexable_ids[] = $link->target_indexable_id;
		}

		$this->update_incoming_links_for_related_indexables( $linked_indexable_ids );
	}

	/**
	 * Gathers all links from content.
	 *
	 * @param string $content The content.
	 *
	 * @return string[] An array of urls.
	 */
	protected function gather_links( $content ) {
		if ( \strpos( $content, 'href' ) === false ) {
			// Nothing to do.
			return [];
		}

		$links  = [];
		$regexp = '<a\s[^>]*href=("??)([^" >]*?)\1[^>]*>';
		// Used modifiers iU to match case insensitive and make greedy quantifiers lazy.
		if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$links[] = \trim( $match[2], "'" );
			}
		}

		return $links;
	}

	/**
	 * Gathers all images from content.
	 *
	 * @param string $content The content.
	 *
	 * @return string[] An array of urls.
	 */
	protected function gather_images( $content ) {
		if ( \strpos( $content, 'src' ) === false ) {
			// Nothing to do.
			return [];
		}

		$images = [];
		$regexp = '<img\s[^>]*src=("??)([^" >]*?)\\1[^>]*>';
		// Used modifiers iU to match case insensitive and make greedy quantifiers lazy.
		if ( \preg_match_all( "/$regexp/iU", $content, $matches, \PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$images[] = \trim( $match[2], "'" );
			}
		}

		return $images;
	}

	/**
	 * Creates link models from lists of URLs and image sources.
	 *
	 * @param Indexable $indexable The indexable.
	 * @param string[]  $links     The link URLs.
	 * @param string[]  $images    The image sources.
	 *
	 * @return SEO_Links[] The link models.
	 */
	protected function create_links( $indexable, $links, $images ) {
		$home_url    = \wp_parse_url( \home_url() );
		$current_url = \wp_parse_url( $indexable->permalink );
		$links       = \array_map(
			function( $link ) use ( $home_url, $indexable ) {
				return $this->create_internal_link( $link, $home_url, $indexable );
			},
			$links
		);
		// Filter out links to the same page with a fragment or query.
		$links = \array_filter(
			$links,
			function ( $link ) use ( $current_url ) {
				return $this->filter_link( $link, $current_url );
			}
		);

		$images = \array_map(
			function( $link ) use ( $home_url, $indexable ) {
				return $this->create_internal_link( $link, $home_url, $indexable, true );
			},
			$images
		);
		return \array_merge( $links, $images );
	}

	/**
	 * Get the post ID based on the link's type and its target's permalink.
	 *
	 * @param string $type      The type of link (either SEO_Links::TYPE_INTERNAL or SEO_Links::TYPE_INTERNAL_IMAGE).
	 * @param string $permalink The permalink of the link's target.
	 *
	 * @return int The post ID.
	 */
	protected function get_post_id( $type, $permalink ) {
		if ( $type === SEO_Links::TYPE_INTERNAL ) {
			return \url_to_postid( $permalink );
		}

		return $this->image_helper->get_attachment_by_url( $permalink );
	}

	/**
	 * Creates an internal link.
	 *
	 * @param string    $url       The url of the link.
	 * @param array     $home_url  The home url, as parsed by wp_parse_url.
	 * @param Indexable $indexable The indexable of the post containing the link.
	 * @param bool      $is_image  Whether or not the link is an image.
	 *
	 * @return SEO_Links The created link.
	 */
	protected function create_internal_link( $url, $home_url, $indexable, $is_image = false ) {
		$parsed_url = \wp_parse_url( $url );
		$link_type  = $this->url_helper->get_link_type( $parsed_url, $home_url, $is_image );

		/**
		 * ORM representing a link in the SEO Links table.
		 *
		 * @var SEO_Links $model
		 */
		$model = $this->seo_links_repository->query()->create(
			[
				'url'          => $url,
				'type'         => $link_type,
				'indexable_id' => $indexable->id,
				'post_id'      => $indexable->object_id,
			]
		);

		$model->parsed_url = $parsed_url;

		if ( $model->type === SEO_Links::TYPE_INTERNAL || $model->type === SEO_Links::TYPE_INTERNAL_IMAGE ) {
			$permalink = $this->get_permalink( $url, $home_url );
			if ( $this->url_helper->is_relative( $permalink ) ) {
				// Make sure we're checking against the absolute URL, and add a trailing slash if the site has a trailing slash in its permalink settings.
				$permalink = $this->url_helper->ensure_absolute_url( \user_trailingslashit( $permalink ) );
			}
			$target = $this->indexable_repository->find_by_permalink( $permalink );

			if ( ! $target ) {
				// If target indexable cannot be found, create one based on the post's post ID.
				$post_id = $this->get_post_id( $model->type, $permalink );
				if ( $post_id && $post_id !== 0 ) {
					$target = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' );
				}
			}

			if ( ! $target ) {
				return $model;
			}

			$model->target_indexable_id = $target->id;
			if ( $target->object_type === 'post' ) {
				$model->target_post_id = $target->object_id;
			}
		}

		if ( $is_image && $model->target_post_id ) {
			$file = \get_attached_file( $model->target_post_id );
			if ( $file ) {
				list( , $width, $height ) = \wp_get_attachment_image_src( $model->target_post_id, 'full' );

				$model->width  = $width;
				$model->height = $height;
				if ( \file_exists( $file ) ) {
					$model->size = \filesize( $file );
				}
				else {
					$model->size = null;
				}
			}
			else {
				$model->width  = 0;
				$model->height = 0;
				$model->size   = 0;
			}
		}

		if ( $model->target_indexable_id ) {
			$model->language = $target->language;
			$model->region   = $target->region;
		}

		return $model;
	}

	/**
	 * Filters out links that point to the same page with a fragment or query.
	 *
	 * @param SEO_Links $link        The link.
	 * @param array     $current_url The url of the page the link is on, as parsed by wp_parse_url.
	 *
	 * @return bool Whether or not the link should be filtered.
	 */
	protected function filter_link( SEO_Links $link, $current_url ) {
		$url = $link->parsed_url;

		// Always keep external links.
		if ( $link->type === SEO_Links::TYPE_EXTERNAL ) {
			return true;
		}

		// Always keep links with an empty path or pointing to other pages.
		if ( isset( $url['path'] ) ) {
			return empty( $url['path'] ) || $url['path'] !== $current_url['path'];
		}

		// Only keep links to the current page without a fragment or query.
		return ( ! isset( $url['fragment'] ) && ! isset( $url['query'] ) );
	}

	/**
	 * Updates the link counts for related indexables.
	 *
	 * @param Indexable   $indexable The indexable.
	 * @param SEO_Links[] $links     The link models.
	 *
	 * @return void
	 */
	protected function update_related_indexables( $indexable, $links ) {
		// Old links were only stored by post id, so remove all old seo links for this post that have no indexable id.
		// This can be removed if we ever fully clear all seo links.
		if ( $indexable->object_type === 'post' ) {
			$this->seo_links_repository->delete_all_by_post_id_where_indexable_id_null( $indexable->object_id );
		}

		$updated_indexable_ids = [];
		$old_links             = $this->seo_links_repository->find_all_by_indexable_id( $indexable->id );

		$links_to_remove = $this->links_diff( $old_links, $links );
		$links_to_add    = $this->links_diff( $links, $old_links );

		if ( ! empty( $links_to_remove ) ) {
			$this->seo_links_repository->delete_many_by_id( \wp_list_pluck( $links_to_remove, 'id' ) );
		}

		if ( ! empty( $links_to_add ) ) {
			$this->seo_links_repository->insert_many( $links_to_add );
		}

		foreach ( $links_to_add as $link ) {
			if ( $link->target_indexable_id ) {
				$updated_indexable_ids[] = $link->target_indexable_id;
			}
		}
		foreach ( $links_to_remove as $link ) {
			$updated_indexable_ids[] = $link->target_indexable_id;
		}

		$this->update_incoming_links_for_related_indexables( $updated_indexable_ids );
	}

	/**
	 * Creates a diff between two arrays of SEO links, based on urls.
	 *
	 * @param SEO_Links[] $links_a The array to compare.
	 * @param SEO_Links[] $links_b The array to compare against.
	 *
	 * @return SEO_Links[] Links that are in $links_a, but not in $links_b.
	 */
	protected function links_diff( $links_a, $links_b ) {
		return \array_udiff(
			$links_a,
			$links_b,
			static function( SEO_Links $link_a, SEO_Links $link_b ) {
				return \strcmp( $link_a->url, $link_b->url );
			}
		);
	}

	/**
	 * Returns the number of internal links in an array of link models.
	 *
	 * @param SEO_Links[] $links The link models.
	 *
	 * @return int The number of internal links.
	 */
	protected function get_internal_link_count( $links ) {
		$internal_link_count = 0;

		foreach ( $links as $link ) {
			if ( $link->type === SEO_Links::TYPE_INTERNAL ) {
				++$internal_link_count;
			}
		}

		return $internal_link_count;
	}

	/**
	 * Returns a cleaned permalink for a given link.
	 *
	 * @param string $link     The raw URL.
	 * @param array  $home_url The home URL, as parsed by wp_parse_url.
	 *
	 * @return string The cleaned permalink.
	 */
	protected function get_permalink( $link, $home_url ) {
		// Get rid of the #anchor.
		$url_split = \explode( '#', $link );
		$link      = $url_split[0];

		// Get rid of URL ?query=string.
		$url_split = \explode( '?', $link );
		$link      = $url_split[0];

		// Set the correct URL scheme.
		$link = \set_url_scheme( $link, $home_url['scheme'] );

		// Add 'www.' if it is absent and should be there.
		if ( \strpos( $home_url['host'], 'www.' ) === 0 && \strpos( $link, '://www.' ) === false ) {
			$link = \str_replace( '://', '://www.', $link );
		}

		// Strip 'www.' if it is present and shouldn't be.
		if ( \strpos( $home_url['host'], 'www.' ) !== 0 ) {
			$link = \str_replace( '://www.', '://', $link );
		}

		return $link;
	}

	/**
	 * Updates incoming link counts for related indexables.
	 *
	 * @param int[] $related_indexable_ids The IDs of all related indexables.
	 *
	 * @return void
	 */
	protected function update_incoming_links_for_related_indexables( $related_indexable_ids ) {
		if ( empty( $related_indexable_ids ) ) {
			return;
		}

		$counts = $this->seo_links_repository->get_incoming_link_counts_for_indexable_ids( $related_indexable_ids );
		foreach ( $counts as $count ) {
			$this->indexable_repository->update_incoming_link_count( $count['target_indexable_id'], $count['incoming'] );
		}
	}
}
indexable-social-image-trait.php000066600000011333151126400400012657 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use WPSEO_Utils;
use Yoast\WP\SEO\Helpers\Image_Helper;
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
use Yoast\WP\SEO\Helpers\Twitter\Image_Helper as Twitter_Image_Helper;
use Yoast\WP\SEO\Models\Indexable;

/**
 * Trait for determine the social image to use in the indexable.
 *
 * Represents the trait used in builders for handling social images.
 */
trait Indexable_Social_Image_Trait {

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

	/**
	 * The Open Graph image helper.
	 *
	 * @var Open_Graph_Image_Helper
	 */
	protected $open_graph_image;

	/**
	 * The Twitter image helper.
	 *
	 * @var Twitter_Image_Helper
	 */
	protected $twitter_image;

	/**
	 * Sets the helpers for the trait.
	 *
	 * @required
	 *
	 * @param Image_Helper            $image            The image helper.
	 * @param Open_Graph_Image_Helper $open_graph_image The Open Graph image helper.
	 * @param Twitter_Image_Helper    $twitter_image    The Twitter image helper.
	 */
	public function set_social_image_helpers(
		Image_Helper $image,
		Open_Graph_Image_Helper $open_graph_image,
		Twitter_Image_Helper $twitter_image
	) {
		$this->image            = $image;
		$this->open_graph_image = $open_graph_image;
		$this->twitter_image    = $twitter_image;
	}

	/**
	 * Sets the alternative on an indexable.
	 *
	 * @param array     $alternative_image The alternative image to set.
	 * @param Indexable $indexable         The indexable to set image for.
	 */
	protected function set_alternative_image( array $alternative_image, Indexable $indexable ) {
		if ( ! empty( $alternative_image['image_id'] ) ) {
			if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) {
				$indexable->open_graph_image_id     = $alternative_image['image_id'];
				$indexable->open_graph_image_source = $alternative_image['source'];

				$this->set_open_graph_image_meta_data( $indexable );
			}

			if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) {
				$indexable->twitter_image        = $this->twitter_image->get_by_id( $alternative_image['image_id'] );
				$indexable->twitter_image_id     = $alternative_image['image_id'];
				$indexable->twitter_image_source = $alternative_image['source'];
			}
		}

		if ( ! empty( $alternative_image['image'] ) ) {
			if ( ! $indexable->open_graph_image_source && ! $indexable->open_graph_image_id ) {
				$indexable->open_graph_image        = $alternative_image['image'];
				$indexable->open_graph_image_source = $alternative_image['source'];
			}

			if ( ! $indexable->twitter_image && ! $indexable->twitter_image_id ) {
				$indexable->twitter_image        = $alternative_image['image'];
				$indexable->twitter_image_source = $alternative_image['source'];
			}
		}
	}

	/**
	 * Sets the Open Graph image meta data for an og image
	 *
	 * @param Indexable $indexable The indexable.
	 */
	protected function set_open_graph_image_meta_data( Indexable $indexable ) {
		if ( ! $indexable->open_graph_image_id ) {
			return;
		}

		$image = $this->open_graph_image->get_image_by_id( $indexable->open_graph_image_id );

		if ( ! empty( $image ) ) {
			$indexable->open_graph_image      = $image['url'];
			$indexable->open_graph_image_meta = WPSEO_Utils::format_json_encode( $image );
		}
	}

	/**
	 * Handles the social images.
	 *
	 * @param Indexable $indexable The indexable to handle.
	 */
	protected function handle_social_images( Indexable $indexable ) {
		// When the image or image id is set.
		if ( $indexable->open_graph_image || $indexable->open_graph_image_id ) {
			$indexable->open_graph_image_source = 'set-by-user';

			$this->set_open_graph_image_meta_data( $indexable );
		}

		if ( $indexable->twitter_image || $indexable->twitter_image_id ) {
			$indexable->twitter_image_source = 'set-by-user';
		}

		if ( $indexable->twitter_image_id ) {
			$indexable->twitter_image = $this->twitter_image->get_by_id( $indexable->twitter_image_id );
		}

		// When image sources are set already.
		if ( $indexable->open_graph_image_source && $indexable->twitter_image_source ) {
			return;
		}

		$alternative_image = $this->find_alternative_image( $indexable );
		if ( ! empty( $alternative_image ) ) {
			$this->set_alternative_image( $alternative_image, $indexable );
		}
	}

	/**
	 * Resets the social images.
	 *
	 * @param Indexable $indexable The indexable to set images for.
	 */
	protected function reset_social_images( Indexable $indexable ) {
		$indexable->open_graph_image        = null;
		$indexable->open_graph_image_id     = null;
		$indexable->open_graph_image_source = null;
		$indexable->open_graph_image_meta   = null;

		$indexable->twitter_image        = null;
		$indexable->twitter_image_id     = null;
		$indexable->twitter_image_source = null;
	}
}
indexable-post-type-archive-builder.php000066600000010431151126400400014211 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use wpdb;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Post type archive builder for the indexables.
 *
 * Formats the post type archive meta to indexable format.
 */
class Indexable_Post_Type_Archive_Builder {

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

	/**
	 * The latest version of the Indexable_Post_Type_Archive_Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Holds the taxonomy helper instance.
	 *
	 * @var Post_Helper
	 */
	protected $post_helper;

	/**
	 * The WPDB instance.
	 *
	 * @var wpdb
	 */
	protected $wpdb;

	/**
	 * Indexable_Post_Type_Archive_Builder constructor.
	 *
	 * @param Options_Helper             $options     The options helper.
	 * @param Indexable_Builder_Versions $versions    The latest version of each Indexable builder.
	 * @param Post_Helper                $post_helper The post helper.
	 * @param wpdb                       $wpdb        The WPDB instance.
	 */
	public function __construct(
		Options_Helper $options,
		Indexable_Builder_Versions $versions,
		Post_Helper $post_helper,
		wpdb $wpdb
	) {
		$this->options     = $options;
		$this->version     = $versions->get_latest_version_for_type( 'post-type-archive' );
		$this->post_helper = $post_helper;
		$this->wpdb        = $wpdb;
	}

	/**
	 * Formats the data.
	 *
	 * @param string    $post_type The post type to build the indexable for.
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return Indexable The extended indexable.
	 */
	public function build( $post_type, Indexable $indexable ) {
		$indexable->object_type       = 'post-type-archive';
		$indexable->object_sub_type   = $post_type;
		$indexable->title             = $this->options->get( 'title-ptarchive-' . $post_type );
		$indexable->description       = $this->options->get( 'metadesc-ptarchive-' . $post_type );
		$indexable->breadcrumb_title  = $this->get_breadcrumb_title( $post_type );
		$indexable->permalink         = \get_post_type_archive_link( $post_type );
		$indexable->is_robots_noindex = $this->options->get( 'noindex-ptarchive-' . $post_type );
		$indexable->is_public         = ( (int) $indexable->is_robots_noindex !== 1 );
		$indexable->blog_id           = \get_current_blog_id();
		$indexable->version           = $this->version;

		$timestamps                      = $this->get_object_timestamps( $post_type );
		$indexable->object_published_at  = $timestamps->published_at;
		$indexable->object_last_modified = $timestamps->last_modified;

		return $indexable;
	}

	/**
	 * Returns the fallback breadcrumb title for a given post.
	 *
	 * @param string $post_type The post type to get the fallback breadcrumb title for.
	 *
	 * @return string
	 */
	private function get_breadcrumb_title( $post_type ) {
		$options_breadcrumb_title = $this->options->get( 'bctitle-ptarchive-' . $post_type );

		if ( $options_breadcrumb_title !== '' ) {
			return $options_breadcrumb_title;
		}

		$post_type_obj = \get_post_type_object( $post_type );

		if ( ! \is_object( $post_type_obj ) ) {
			return '';
		}

		if ( isset( $post_type_obj->label ) && $post_type_obj->label !== '' ) {
			return $post_type_obj->label;
		}

		if ( isset( $post_type_obj->labels->menu_name ) && $post_type_obj->labels->menu_name !== '' ) {
			return $post_type_obj->labels->menu_name;
		}

		return $post_type_obj->name;
	}

	/**
	 * Returns the timestamps for a given post type.
	 *
	 * @param string $post_type The post type.
	 *
	 * @return object An object with last_modified and published_at timestamps.
	 */
	protected function get_object_timestamps( $post_type ) {
		$post_statuses = $this->post_helper->get_public_post_statuses();

		$sql = "
			SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at
			FROM {$this->wpdb->posts} AS p
			WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
				AND p.post_password = ''
				AND p.post_type = %s
		";

		$replacements = \array_merge( $post_statuses, [ $post_type ] );

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare.
		return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) );
	}
}
indexable-system-page-builder.php000066600000004146151126400400013072 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * System page builder for the indexables.
 *
 * Formats system pages ( search and error ) meta to indexable format.
 */
class Indexable_System_Page_Builder {

	/**
	 * Mapping of object type to title option keys.
	 */
	const OPTION_MAPPING = [
		'search-result' => [
			'title'            => 'title-search-wpseo',
		],
		'404'           => [
			'title'            => 'title-404-wpseo',
			'breadcrumb_title' => 'breadcrumbs-404crumb',
		],
	];

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

	/**
	 * The latest version of the Indexable_System_Page_Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Indexable_System_Page_Builder constructor.
	 *
	 * @param Options_Helper             $options  The options helper.
	 * @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder.
	 */
	public function __construct(
		Options_Helper $options,
		Indexable_Builder_Versions $versions
	) {
		$this->options = $options;
		$this->version = $versions->get_latest_version_for_type( 'system-page' );
	}

	/**
	 * Formats the data.
	 *
	 * @param string    $object_sub_type The object sub type of the system page.
	 * @param Indexable $indexable       The indexable to format.
	 *
	 * @return Indexable The extended indexable.
	 */
	public function build( $object_sub_type, Indexable $indexable ) {
		$indexable->object_type       = 'system-page';
		$indexable->object_sub_type   = $object_sub_type;
		$indexable->title             = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['title'] );
		$indexable->is_robots_noindex = true;
		$indexable->blog_id           = \get_current_blog_id();

		if ( \array_key_exists( 'breadcrumb_title', static::OPTION_MAPPING[ $object_sub_type ] ) ) {
			$indexable->breadcrumb_title = $this->options->get( static::OPTION_MAPPING[ $object_sub_type ]['breadcrumb_title'] );
		}

		$indexable->version = $this->version;

		return $indexable;
	}
}
indexable-date-archive-builder.php000066600000003234151126400400013165 0ustar00<?php

namespace Yoast\WP\SEO\Builders;

use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;

/**
 * Date Archive Builder for the indexables.
 *
 * Formats the date archive meta to indexable format.
 */
class Indexable_Date_Archive_Builder {

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

	/**
	 * The latest version of the Indexable_Date_Archive_Builder.
	 *
	 * @var int
	 */
	protected $version;

	/**
	 * Indexable_Date_Archive_Builder constructor.
	 *
	 * @param Options_Helper             $options  The options helper.
	 * @param Indexable_Builder_Versions $versions The latest version for all indexable builders.
	 */
	public function __construct(
		Options_Helper $options,
		Indexable_Builder_Versions $versions
	) {
		$this->options = $options;
		$this->version = $versions->get_latest_version_for_type( 'date-archive' );
	}

	/**
	 * Formats the data.
	 *
	 * @param Indexable $indexable The indexable to format.
	 *
	 * @return Indexable The extended indexable.
	 */
	public function build( $indexable ) {
		$indexable->object_type       = 'date-archive';
		$indexable->title             = $this->options->get( 'title-archive-wpseo' );
		$indexable->description       = $this->options->get( 'metadesc-archive-wpseo' );
		$indexable->is_robots_noindex = $this->options->get( 'noindex-archive-wpseo' );
		$indexable->is_public         = ( (int) $indexable->is_robots_noindex !== 1 );
		$indexable->blog_id           = \get_current_blog_id();
		$indexable->permalink         = null;
		$indexable->version           = $this->version;

		return $indexable;
	}
}
class-elementor-full-width-templates.php000066600000006250151136416460014441 0ustar00<?php
/**
 * Class ThemeIsle\FullWidthTemplates\Elementor
 *
 * @package     ThemeIsle\FullWidthTemplates\Elementor
 * @copyright   Copyright (c) 2017, Andrei Lupu
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0.0
 */

namespace ThemeIsle\FullWidthTemplates;

class Elementor {
	/**
	 * @var Elementor
	 */
	public static $instance = null;

	protected function init(){
		// for the blank template
		add_action( 'fwpt_blank_before_content', array( $this, 'render_blank_before_content' ) );
		add_action( 'fwpt_blank_content', array( $this, 'render_content' ) );
		add_action( 'fwpt_blank_after_content', array( $this, 'render_blank_after_content' ) );

		// for the standard template
		add_action( 'fwpt_std_content', array( $this, 'render_content' ) );
		add_action( 'fwpt_std_before_content', array( $this, 'render_std_before_content' ) );
		add_action( 'fwpt_std_after_content', array( $this, 'render_std_after_content' ) );

		// @TODO We should move this into a function to keep compat?
		add_action( 'elementor_page_elements', array( $this, 'elementor_page_content' ), 20 );
	}

	/**
	 * Display the WordPress loop
	 */
	public function render_content() {
		while ( have_posts() ) : the_post();
			do_action( 'elementor_page_elements' ); // Give your elements priorities so that they hook in the right place.
		endwhile;
	}

	/**
	 * Display the header of the Blank template
	 */
	public function render_blank_before_content() {
		do_action( 'elementor_content_body_before' );
		do_action( 'elementor_before_content_wrapper' );
	}

	/**
	 * Display the footer of the blank template
	 */
	public function render_blank_after_content() {
		do_action( 'elementor_after_content_wrapper' );
		do_action( 'elementor_content_body_after' );
	}

	/**
	 * Display the header of the standard template
	 */
	public function render_std_before_content() {
		get_header();
		do_action( 'elementor_before_content_wrapper' );
	}

	/**
	 * Display the footer of the standard template
	 */
	public function render_std_after_content() {
		do_action( 'elementor_after_content_wrapper' );
		get_footer();
	}

	// @TODO We should move this into a function to keep compat?
	function elementor_page_content() {
		the_content();
	}

	/**
	 * @static
	 * @since 1.0.0
	 * @access public
	 * @return Elementor
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
			self::$instance->init();
		}

		return self::$instance;
	}

	/**
	 * Throw error on object clone
	 *
	 * The whole idea of the singleton design pattern is that there is a single
	 * object therefore, we don't want the object to be cloned.
	 *
	 * @access public
	 * @since 1.0.0
	 * @return void
	 */
	public function __clone() {
		// Cloning instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'themeisle-companion' ), '1.0.0' );
	}

	/**
	 * Disable unserializing of the class
	 *
	 * @access public
	 * @since 1.0.0
	 * @return void
	 */
	public function __wakeup() {
		// Unserializing instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'themeisle-companion' ), '1.0.0' );
	}
}class-none-full-width-templates.php000066600000004031151140222360013366 0ustar00<?php
/**
 * Class ThemeIsle\FullWidthTemplates\Elementor
 *
 * @package     ThemeIsle\FullWidthTemplates\Elementor
 * @copyright   Copyright (c) 2017, Andrei Lupu
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0.0
 */

namespace ThemeIsle\FullWidthTemplates;

class None {
	/**
	 * @var Elementor
	 */
	public static $instance = null;

	protected function init(){
		// for the standard template
		add_action( 'fwpt_std_content', array( $this, 'render_content' ) );
		add_action( 'fwpt_std_before_content', array( $this, 'render_std_before_content' ) );
		add_action( 'fwpt_std_after_content', array( $this, 'render_std_after_content' ) );

	}

	/**
	 * Display the WordPress loop
	 */
	public function render_content() {
		while ( have_posts() ) : the_post();
			the_content();
		endwhile;
	}


	/**
	 * Display the header of the standard template
	 */
	public function render_std_before_content() {
		get_header();
	}

	/**
	 * Display the footer of the standard template
	 */
	public function render_std_after_content() {
		get_footer();
	}


	/**
	 * @static
	 * @since 1.0.0
	 * @access public
	 * @return Elementor
	 */
	public static function instance() {
		if ( null ===  self::$instance ) {
			self::$instance = new self();
			self::$instance->init();
		}

		return self::$instance;
	}

	/**
	 * Throw error on object clone
	 *
	 * The whole idea of the singleton design pattern is that there is a single
	 * object therefore, we don't want the object to be cloned.
	 *
	 * @access public
	 * @since 1.0.0
	 * @return void
	 */
	public function __clone() {
		// Cloning instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'themeisle-companion' ), '1.0.0' );
	}

	/**
	 * Disable unserializing of the class
	 *
	 * @access public
	 * @since 1.0.0
	 * @return void
	 */
	public function __wakeup() {
		// Unserializing instances of the class is forbidden.
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'themeisle-companion' ), '1.0.0' );
	}
}