first commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
<?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 link builder
|
||||
*
|
||||
* @var Indexable_Link_Builder
|
||||
*/
|
||||
private $link_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.
|
||||
* @param Indexable_Link_Builder $link_builder The link builder for creating missing SEO links.
|
||||
*/
|
||||
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,
|
||||
Indexable_Link_Builder $link_builder
|
||||
) {
|
||||
$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;
|
||||
$this->link_builder = $link_builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the indexable type is one that is not supposed to have object ID for.
|
||||
*
|
||||
* @param string $type The type of the indexable.
|
||||
*
|
||||
* @return bool Whether the indexable type is one that is not supposed to have object ID for.
|
||||
*/
|
||||
protected function is_type_with_no_id( $type ) {
|
||||
return \in_array( $type, [ 'home-page', 'date-archive', 'post-type-archive', 'system-page' ], true );
|
||||
}
|
||||
|
||||
// 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 );
|
||||
|
||||
// Save indexable, to make sure it can be queried when building related objects like the author indexable and hierarchy.
|
||||
$indexable = $this->save_indexable( $indexable, $indexable_before );
|
||||
|
||||
// For attachments, we have to make sure to patch any potentially previously cleaned up SEO links.
|
||||
if ( \is_a( $indexable, Indexable::class ) && $indexable->object_sub_type === 'attachment' ) {
|
||||
$this->link_builder->patch_seo_links( $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 );
|
||||
|
||||
$this->maybe_build_author_indexable( $indexable->author_id );
|
||||
|
||||
// The indexable is already saved, so return early.
|
||||
return $indexable;
|
||||
|
||||
case 'user':
|
||||
$indexable = $this->author_builder->build( $indexable->object_id, $indexable );
|
||||
break;
|
||||
|
||||
case 'term':
|
||||
$indexable = $this->term_builder->build( $indexable->object_id, $indexable );
|
||||
|
||||
// Save indexable, to make sure it can be queried when building hierarchy.
|
||||
$indexable = $this->save_indexable( $indexable, $indexable_before );
|
||||
|
||||
$this->hierarchy_builder->build( $indexable );
|
||||
|
||||
// The indexable is already saved, so return early.
|
||||
return $indexable;
|
||||
|
||||
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 ) {
|
||||
if ( ! $this->is_type_with_no_id( $indexable->object_type ) && ( ! isset( $indexable->object_id ) || \is_null( $indexable->object_id ) ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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 ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WPSEO_Image_Utils;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
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\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 options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
protected $options_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.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
*/
|
||||
public function __construct(
|
||||
SEO_Links_Repository $seo_links_repository,
|
||||
Url_Helper $url_helper,
|
||||
Post_Helper $post_helper,
|
||||
Options_Helper $options_helper
|
||||
) {
|
||||
$this->seo_links_repository = $seo_links_repository;
|
||||
$this->url_helper = $url_helper;
|
||||
$this->post_helper = $post_helper;
|
||||
$this->options_helper = $options_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( ']]>', ']]>', $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 ) {
|
||||
if ( $link->target_indexable_id ) {
|
||||
$linked_indexable_ids[] = $link->target_indexable_id;
|
||||
}
|
||||
}
|
||||
|
||||
$this->update_incoming_links_for_related_indexables( $linked_indexable_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes existing SEO links that are supposed to have a target indexable but don't, because of prior indexable cleanup.
|
||||
*
|
||||
* @param Indexable $indexable The indexable to be the target of SEO Links.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function patch_seo_links( Indexable $indexable ) {
|
||||
if ( ! empty( $indexable->id ) && ! empty( $indexable->object_id ) ) {
|
||||
$links = $this->seo_links_repository->find_all_by_target_post_id( $indexable->object_id );
|
||||
|
||||
$updated_indexable = false;
|
||||
foreach ( $links as $link ) {
|
||||
if ( \is_a( $link, SEO_Links::class ) && empty( $link->target_indexable_id ) ) {
|
||||
// Since that post ID exists in an SEO link but has no target_indexable_id, it's probably because of prior indexable cleanup.
|
||||
$this->seo_links_repository->update_target_indexable_id( $link->id, $indexable->id );
|
||||
$updated_indexable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $updated_indexable ) {
|
||||
$updated_indexable_id = [ $indexable->id ];
|
||||
$this->update_incoming_links_for_related_indexables( $updated_indexable_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
$permalink = $this->build_permalink( $url, $home_url );
|
||||
|
||||
return $this->enhance_link_from_indexable( $model, $permalink );
|
||||
}
|
||||
|
||||
if ( $model->type === SEO_Links::TYPE_INTERNAL_IMAGE ) {
|
||||
$permalink = $this->build_permalink( $url, $home_url );
|
||||
|
||||
if ( ! $this->options_helper->get( 'disable-attachment' ) ) {
|
||||
$model = $this->enhance_link_from_indexable( $model, $permalink );
|
||||
}
|
||||
else {
|
||||
$target_post_id = WPSEO_Image_Utils::get_attachment_by_url( $permalink );
|
||||
|
||||
if ( ! empty( $target_post_id ) ) {
|
||||
$model->target_post_id = $target_post_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $model->target_post_id ) {
|
||||
$file = \get_attached_file( $model->target_post_id );
|
||||
|
||||
if ( $file ) {
|
||||
if ( \file_exists( $file ) ) {
|
||||
$model->size = \filesize( $file );
|
||||
}
|
||||
else {
|
||||
$model->size = null;
|
||||
}
|
||||
|
||||
list( , $width, $height ) = \wp_get_attachment_image_src( $model->target_post_id, 'full' );
|
||||
$model->width = $width;
|
||||
$model->height = $height;
|
||||
}
|
||||
else {
|
||||
$model->width = 0;
|
||||
$model->height = 0;
|
||||
$model->size = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the link model with information from its indexable.
|
||||
*
|
||||
* @param SEO_Links $model The link's model.
|
||||
* @param string $permalink The link's permalink.
|
||||
*
|
||||
* @return SEO_Links The enhanced link model.
|
||||
*/
|
||||
protected function enhance_link_from_indexable( $model, $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 ( $model->target_indexable_id ) {
|
||||
$model->language = $target->language;
|
||||
$model->region = $target->region;
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the link's permalink.
|
||||
*
|
||||
* @param string $url The url of the link.
|
||||
* @param array $home_url The home url, as parsed by wp_parse_url.
|
||||
*
|
||||
* @return string The link's permalink.
|
||||
*/
|
||||
protected function build_permalink( $url, $home_url ) {
|
||||
$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 ) );
|
||||
}
|
||||
|
||||
return $permalink;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
if ( $link->target_indexable_id ) {
|
||||
$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'] );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Post_Not_Built_Exception;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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->inclusive_language_score = (int) $this->meta->get_value( 'inclusive_language_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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Builders;
|
||||
|
||||
use wpdb;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Post_Type_Not_Built_Exception;
|
||||
use Yoast\WP\SEO\Helpers\Options_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\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 post helper instance.
|
||||
*
|
||||
* @var Post_Helper
|
||||
*/
|
||||
protected $post_helper;
|
||||
|
||||
/**
|
||||
* Holds the post type helper instance.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
protected $post_type_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 Post_Type_Helper $post_type_helper The post type helper.
|
||||
* @param wpdb $wpdb The WPDB instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Options_Helper $options,
|
||||
Indexable_Builder_Versions $versions,
|
||||
Post_Helper $post_helper,
|
||||
Post_Type_Helper $post_type_helper,
|
||||
wpdb $wpdb
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->version = $versions->get_latest_version_for_type( 'post-type-archive' );
|
||||
$this->post_helper = $post_helper;
|
||||
$this->post_type_helper = $post_type_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.
|
||||
* @throws \Yoast\WP\SEO\Exceptions\Indexable\Post_Type_Not_Built_Exception Throws exception if the post type is excluded.
|
||||
*/
|
||||
public function build( $post_type, Indexable $indexable ) {
|
||||
if ( ! $this->post_type_helper->is_post_type_archive_indexable( $post_type ) ) {
|
||||
throw Post_Type_Not_Built_Exception::because_not_indexable( $post_type );
|
||||
}
|
||||
|
||||
$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 ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?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_Built_Exception;
|
||||
use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Found_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_inclusive_language_score' => 'inclusive_language_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 ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user