diff --git a/composer.json b/composer.json index 5924592f106..8c80b81bdb6 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,7 @@ "Yoast\\WP\\SEO\\Composer\\Actions::check_coding_standards" ], "check-cs-thresholds": [ - "@putenv YOASTCS_THRESHOLD_ERRORS=252", + "@putenv YOASTCS_THRESHOLD_ERRORS=253", "@putenv YOASTCS_THRESHOLD_WARNINGS=220", "Yoast\\WP\\SEO\\Composer\\Actions::check_cs_thresholds" ], diff --git a/lib/migrations/adapter.php b/lib/migrations/adapter.php index 152ad01ae60..3cc1b634dfa 100644 --- a/lib/migrations/adapter.php +++ b/lib/migrations/adapter.php @@ -331,7 +331,7 @@ public function select_one( $query ) { return false; } - return $wpdb->last_result[0]; + return (array) $wpdb->last_result[0]; } return false; diff --git a/lib/model.php b/lib/model.php index 41c90952bc7..b5de2537b6c 100644 --- a/lib/model.php +++ b/lib/model.php @@ -95,6 +95,13 @@ class Model implements JsonSerializable { */ protected $float_columns = []; + /** + * Which columns are deprecated. + * + * @var array + */ + protected $deprecated_columns = []; + /** * Hacks around the Model to provide WordPress prefix to tables. * @@ -507,15 +514,18 @@ public function set_orm( $orm ) { * @return mixed The value of the property */ public function __get( $property ) { - $value = $this->orm->get( $property ); + $original_property = $property; + $property = $this->handle_deprecation( $property ); + $value = $this->orm->get( $property ); - if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) { + // The types of a deprecated property and its replacement may differ. To prevent this kind of deprecation from being a breaking change, use the old/originally requested type. + if ( $value !== null && \in_array( $original_property, $this->boolean_columns, true ) ) { return (bool) $value; } - if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) { + if ( $value !== null && \in_array( $original_property, $this->int_columns, true ) ) { return (int) $value; } - if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) { + if ( $value !== null && \in_array( $original_property, $this->float_columns, true ) ) { return (float) $value; } @@ -531,6 +541,7 @@ public function __get( $property ) { * @return void */ public function __set( $property, $value ) { + $property = $this->handle_deprecation( $property ); if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) { $value = ( $value ) ? '1' : '0'; } @@ -552,6 +563,7 @@ public function __set( $property, $value ) { * @return void */ public function __unset( $property ) { + $property = $this->handle_deprecation( $property ); $this->orm->__unset( $property ); } @@ -582,6 +594,7 @@ public function __debugInfo() { * @return bool True when value is set. */ public function __isset( $property ) { + $property = $this->handle_deprecation( $property ); return $this->orm->__isset( $property ); } @@ -593,6 +606,8 @@ public function __isset( $property ) { * @return string The value of a property. */ public function get( $property ) { + $property = $this->handle_deprecation( $property ); + return $this->orm->get( $property ); } @@ -605,6 +620,37 @@ public function get( $property ) { * @return static Current object. */ public function set( $property, $value = null ) { + $property = $this->handle_deprecation( $property ); + $this->orm->set( $property, $value ); + + return $this; + } + + /** + * Setter method for model properties that are deprecated, but still need to updated while they wait to be removed. + * + * @param string|array $property The property to set. + * @param string|null $value The value to give. + * + * @return static Current object. + * + * @internal + */ + public function set_deprecated_property( $property, $value = null ) { + if ( ! array_key_exists( $property, $this->deprecated_columns ) ) { + return $this; + } + + if ( $value !== null && \in_array( $property, $this->boolean_columns, true ) ) { + $value = ( $value ) ? '1' : '0'; + } + if ( $value !== null && \in_array( $property, $this->int_columns, true ) ) { + $value = (string) $value; + } + if ( $value !== null && \in_array( $property, $this->float_columns, true ) ) { + $value = (string) $value; + } + $this->orm->set( $property, $value ); return $this; @@ -619,6 +665,7 @@ public function set( $property, $value = null ) { * @return static Current object. */ public function set_expr( $property, $value = null ) { + $property = $this->handle_deprecation( $property ); $this->orm->set_expr( $property, $value ); return $this; @@ -632,6 +679,7 @@ public function set_expr( $property, $value = null ) { * @return bool True when field is changed. */ public function is_dirty( $property ) { + $property = $this->handle_deprecation( $property ); return $this->orm->is_dirty( $property ); } @@ -722,4 +770,37 @@ public static function __callStatic( $method, $arguments ) { return \call_user_func_array( [ $model, $method ], $arguments ); } + + /** + * Checks if a property is deprecated and handles notifications and replacement. + * + * @param string $property The property to check for deprecation. + * + * @return string The deprecated property name. Or its replacement if available. + */ + protected function handle_deprecation( $property ) { + if ( ! array_key_exists( $property, $this->deprecated_columns ) ) { + return $property; + } + $deprecation = $this->deprecated_columns[ $property ]; + + if ( ! empty( $deprecation['replacement'] ) ) { + // There is no _deprecated_property() function, so we use the closest match _deprecated_argument(). + \_deprecated_argument( + __FUNCTION__, + esc_html( $deprecation['since'] ), + 'Use the \"' . esc_html( $deprecation['replacement'] ) . '\" property instead of \"' . esc_html( $property ) . '\" ' + ); + + if ( $deprecation['automatically_use_replacement'] === true ) { + return $deprecation['replacement']; + } + + return $property; + } + // There is no _deprecated_property() function, so we use the closest match _deprecated_argument(). + \_deprecated_argument( __FUNCTION__, esc_html( $deprecation['since'] ), 'The \"' . esc_html( $property ) . '\" property will be removed in a future version' ); + + return $property; + } } diff --git a/lib/orm.php b/lib/orm.php index 41d7ed8cf90..bc75ef87dce 100644 --- a/lib/orm.php +++ b/lib/orm.php @@ -49,7 +49,6 @@ * @see http://www.php-fig.org/psr/psr-1/ */ class ORM implements \ArrayAccess { - /* * --- CLASS CONSTANTS --- */ diff --git a/src/builders/indexable-author-builder.php b/src/builders/indexable-author-builder.php index 614be73577c..984f97cc001 100644 --- a/src/builders/indexable-author-builder.php +++ b/src/builders/indexable-author-builder.php @@ -87,22 +87,35 @@ public function build( $user_id, Indexable $indexable ) { $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(); + $indexable->set_deprecated_property( 'is_public', ( $indexable->is_robots_noindex ) ? false : null ); $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 = $this->set_aggregate_values( $indexable ); $indexable->version = $this->version; return $indexable; } + /** + * Sets the aggregate values for an author indexable. + * + * @param Indexable $indexable The indexable to set the aggregates for. + * + * @return Indexable The indexable with set aggregates. + */ + public function set_aggregate_values( Indexable $indexable ) { + $aggregates = $this->get_public_post_archive_aggregates( $indexable->object_id ); + $indexable->object_published_at = $aggregates->first_published_at; + $indexable->object_last_modified = max( $indexable->object_last_modified, $aggregates->most_recent_last_modified ); + $indexable->number_of_publicly_viewable_posts = $aggregates->number_of_public_posts; + + return $indexable; + } + /** * Retrieves the meta data for this indexable. * @@ -168,24 +181,31 @@ protected function find_alternative_image( Indexable $indexable ) { } /** - * Returns the timestamps for a given author. + * Returns public post aggregates for a given author. + * We don't consider password protected posts to be public. This helps when building sitemaps for instance, where + * password protected posts are also excluded. * - * @param int $author_id The author ID. + * @param int $author_id The author ID. * - * @return object An object with last_modified and published_at timestamps. + * @return object An object with the number of public posts, most recent last modified and first published at timestamps. */ - protected function get_object_timestamps( $author_id ) { + protected function get_public_post_archive_aggregates( $author_id ) { $post_statuses = $this->post_helper->get_public_post_statuses(); + $post_types = $this->author_archive->get_author_archive_post_types(); $sql = " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_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 = '' + WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND p.post_author = %d - "; + AND p.post_password = "" + AND p.post_type IN (' . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ') + '; - $replacements = \array_merge( $post_statuses, [ $author_id ] ); + $replacements = \array_merge( $post_statuses, [ $author_id ], $post_types ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare. return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) ); diff --git a/src/builders/indexable-builder.php b/src/builders/indexable-builder.php index 9dd97e8707a..5e540dd13a5 100644 --- a/src/builders/indexable-builder.php +++ b/src/builders/indexable-builder.php @@ -219,6 +219,7 @@ public function build_for_post_type_archive( $post_type, $indexable = false ) { 'object_type' => 'post-type-archive', 'object_sub_type' => $post_type, ]; + return $this->build( $indexable, $defaults ); } @@ -235,6 +236,7 @@ public function build_for_system_page( $page_type, $indexable = false ) { 'object_type' => 'system-page', 'object_sub_type' => $page_type, ]; + return $this->build( $indexable, $defaults ); } @@ -372,8 +374,7 @@ public function build( $indexable, $defaults = null ) { } return $this->save_indexable( $indexable, $indexable_before ); - } - catch ( Source_Exception $exception ) { + } catch ( Source_Exception $exception ) { /** * The current indexable could not be indexed. Create a placeholder indexable, so we can * skip this indexable in future indexing runs. @@ -396,4 +397,43 @@ public function build( $indexable, $defaults = null ) { return $this->save_indexable( $indexable, $indexable_before ); } } + + /** + * Recalculates indexable aggregates. + * + * @param Indexable $indexable The Indexable to (re)build. + * + * @return Indexable The resulting Indexable. + */ + public function recalculate_aggregates( Indexable $indexable ) { + // Backup the previous Indexable, if there was one. + $indexable_before = $this->deep_copy_indexable( $indexable ); + + switch ( $indexable->object_type ) { + case 'system-page': + case 'date-archive': + case 'post': + // Nothing to recalculate. + break; + + case 'user': + $indexable = $this->author_builder->set_aggregate_values( $indexable ); + break; + + case 'term': + $indexable = $this->term_builder->set_aggregate_values( $indexable ); + break; + + case 'home-page': + $indexable = $this->home_page_builder->set_aggregate_values( $indexable ); + break; + + case 'post-type-archive': + $indexable = $this->post_type_archive_builder->set_aggregate_values( $indexable ); + break; + } + + return $this->save_indexable( $indexable, $indexable_before ); + } } + diff --git a/src/builders/indexable-date-archive-builder.php b/src/builders/indexable-date-archive-builder.php index 944a03e263e..2d06514f54c 100644 --- a/src/builders/indexable-date-archive-builder.php +++ b/src/builders/indexable-date-archive-builder.php @@ -51,14 +51,15 @@ public function __construct( * @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; + $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_publicly_viewable = ! (bool) $this->options->get( 'disable-date' ); + $indexable->blog_id = \get_current_blog_id(); + $indexable->permalink = null; + $indexable->version = $this->version; + $indexable->set_deprecated_property( 'is_public', (int) $indexable->is_robots_noindex !== 1 ); return $indexable; } diff --git a/src/builders/indexable-home-page-builder.php b/src/builders/indexable-home-page-builder.php index 8cc5d49afe2..0b0edb234dc 100644 --- a/src/builders/indexable-home-page-builder.php +++ b/src/builders/indexable-home-page-builder.php @@ -96,7 +96,8 @@ public function build( $indexable ) { $indexable->description = \get_bloginfo( 'description' ); } - $indexable->is_robots_noindex = \get_option( 'blog_public' ) === '0'; + $indexable->is_robots_noindex = \get_option( 'blog_public' ) === '0'; + $indexable->is_publicly_viewable = true; $indexable->open_graph_title = $this->options->get( 'open_graph_frontpage_title' ); $indexable->open_graph_image = $this->options->get( 'open_graph_frontpage_image' ); @@ -114,9 +115,7 @@ public function build( $indexable ) { $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 = $this->set_aggregate_values( $indexable ); $indexable->version = $this->version; @@ -124,18 +123,36 @@ public function build( $indexable ) { } /** - * Returns the timestamps for the homepage. + * Sets the aggregate values for a home page indexable. * - * @return object An object with last_modified and published_at timestamps. + * @param Indexable $indexable The indexable to set the aggregates for. + * + * @return Indexable The indexable with set aggregates. + */ + public function set_aggregate_values( Indexable $indexable ) { + $aggregates = $this->get_public_post_archive_aggregates(); + $indexable->object_published_at = $aggregates->first_published_at; + $indexable->object_last_modified = max( $indexable->object_last_modified, $aggregates->most_recent_last_modified ); + $indexable->number_of_publicly_viewable_posts = $aggregates->number_of_public_posts; + + return $indexable; + } + + /** + * Returns public post aggregates for the homepage. + * + * @return object An object with the number of posts, most recent last modified and first published at timestamps. */ - protected function get_object_timestamps() { + protected function get_public_post_archive_aggregates() { $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 + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_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' "; diff --git a/src/builders/indexable-post-builder.php b/src/builders/indexable-post-builder.php index 71417b8ed06..2611ce98141 100644 --- a/src/builders/indexable-post-builder.php +++ b/src/builders/indexable-post-builder.php @@ -142,12 +142,14 @@ public function build( $post_id, $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->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_publicly_viewable = $this->is_post_publicly_viewable( $post ); + $indexable->number_of_publicly_viewable_posts = 0; + $indexable->blog_id = \get_current_blog_id(); + + $indexable->set_deprecated_property( 'is_public', $this->is_public( $indexable ) ); $indexable->schema_page_type = $this->get_meta_value( $post_id, 'schema_page_type' ); $indexable->schema_article_type = $this->get_meta_value( $post_id, 'schema_article_type' ); @@ -155,6 +157,7 @@ public function build( $post_id, $indexable ) { $indexable->object_last_modified = $post->post_modified_gmt; $indexable->object_published_at = $post->post_date_gmt; + $indexable->version = $this->version; return $indexable; @@ -181,7 +184,10 @@ protected function get_permalink( $post_type, $post_id ) { * * @param Indexable $indexable The indexable. * - * @return bool|null Whether or not the post type is public. Null if no override is set. + * @return bool|null Whether the post type is public. Null if no override is set. + * + * @codeCoverageIgnore + * @deprecated 17.9 */ protected function is_public( $indexable ) { if ( $indexable->is_protected === true ) { @@ -214,6 +220,9 @@ protected function is_public( $indexable ) { * @param Indexable $indexable The indexable. * * @return bool|null False when it has no parent. Null when it has a parent. + * + * @codeCoverageIgnore + * @deprecated 17.9 */ protected function is_public_attachment( $indexable ) { // If the attachment has no parent, it should not be public. @@ -225,38 +234,6 @@ protected function is_public_attachment( $indexable ) { 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. * @@ -405,6 +382,60 @@ protected function get_number_of_pages_for_post( $post ) { return $number_of_pages; } + /** + * Determine whether a post is publicly viewable. + * + * Posts are considered publicly viewable if both the post status and post type + * are viewable. + * + * @param WP_Post $post The post. + * + * @return bool Whether the post is publicly viewable. + * + * @see \is_post_publicly_viewable Polyfill for WP 5.6. This function was introduced to WP core in 5.7. + */ + protected function is_post_publicly_viewable( $post ) { + if ( ! $post ) { + return false; + } + + $post_type = \get_post_type( $post ); + $post_status = \get_post_status( $post ); + + return \is_post_type_viewable( $post_type ) && $this->is_post_status_viewable( $post_status ); + } + + /** + * Determine whether a post status is considered "viewable". + * + * For built-in post statuses such as publish and private, the 'public' value will be evaluted. + * For all others, the 'publicly_queryable' value will be used. + * + * @param string|stdClass $post_status Post status name or object. + * + * @return bool Whether the post status should be considered viewable. + * + * @see \is_post_status_viewable Polyfill for WP 5.6. This function was introduced to WP core in 5.7. + */ + protected function is_post_status_viewable( $post_status ) { + if ( is_scalar( $post_status ) ) { + $post_status = \get_post_status_object( $post_status ); + if ( ! $post_status ) { + return false; + } + } + + if ( + ! is_object( $post_status ) + || $post_status->internal + || $post_status->protected + ) { + return false; + } + + return $post_status->publicly_queryable || ( $post_status->_builtin && $post_status->public ); + } + /** * Checks whether an indexable should be built for this post. * diff --git a/src/builders/indexable-post-type-archive-builder.php b/src/builders/indexable-post-type-archive-builder.php index 3c0f09059d8..e4708f6d4c5 100644 --- a/src/builders/indexable-post-type-archive-builder.php +++ b/src/builders/indexable-post-type-archive-builder.php @@ -5,6 +5,7 @@ use wpdb; 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; @@ -38,6 +39,13 @@ class Indexable_Post_Type_Archive_Builder { */ protected $post_helper; + /** + * A helper for post types. + * + * @var Post_Type_Helper + */ + private $post_type_helper; + /** * The WPDB instance. * @@ -48,21 +56,24 @@ class Indexable_Post_Type_Archive_Builder { /** * Indexable_Post_Type_Archive_Builder constructor. * - * @param Options_Helper $options The options helper. - * @param Indexable_Builder_Versions $versions The latest version of each Indexable builder. - * @param Post_Helper $post_helper The post helper. - * @param wpdb $wpdb The WPDB instance. + * @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->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; } /** @@ -74,20 +85,36 @@ public function __construct( * @return Indexable The extended indexable. */ public function build( $post_type, Indexable $indexable ) { - $indexable->object_type = 'post-type-archive'; - $indexable->object_sub_type = $post_type; - $indexable->title = $this->options->get( 'title-ptarchive-' . $post_type ); - $indexable->description = $this->options->get( 'metadesc-ptarchive-' . $post_type ); - $indexable->breadcrumb_title = $this->get_breadcrumb_title( $post_type ); - $indexable->permalink = \get_post_type_archive_link( $post_type ); - $indexable->is_robots_noindex = $this->options->get( 'noindex-ptarchive-' . $post_type ); - $indexable->is_public = ( (int) $indexable->is_robots_noindex !== 1 ); - $indexable->blog_id = \get_current_blog_id(); - $indexable->version = $this->version; - - $timestamps = $this->get_object_timestamps( $post_type ); - $indexable->object_published_at = $timestamps->published_at; - $indexable->object_last_modified = $timestamps->last_modified; + $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 = (bool) $this->options->get( 'noindex-ptarchive-' . $post_type ); + $indexable->blog_id = \get_current_blog_id(); + $indexable->is_publicly_viewable = $this->post_type_helper->has_publicly_viewable_archive( $post_type ); + $indexable->set_deprecated_property( 'is_public', ( (int) $indexable->is_robots_noindex !== 1 ) ); + + $indexable = $this->set_aggregate_values( $indexable ); + + $indexable->version = $this->version; + + return $indexable; + } + + /** + * Sets the aggregate values for a post type archive indexable. + * + * @param Indexable $indexable The indexable to set the aggregates for. + * + * @return Indexable The indexable with set aggregates. + */ + public function set_aggregate_values( Indexable $indexable ) { + $aggregates = $this->get_public_post_archive_aggregates( $indexable->object_sub_type ); + $indexable->object_published_at = $aggregates->first_published_at; + $indexable->object_last_modified = max( $indexable->object_last_modified, $aggregates->most_recent_last_modified ); + $indexable->number_of_publicly_viewable_posts = $aggregates->number_of_public_posts; return $indexable; } @@ -124,22 +151,27 @@ private function get_breadcrumb_title( $post_type ) { } /** - * Returns the timestamps for a given post type. + * Returns public post aggregates for a given post type. + * We don't consider password protected posts to be public. This helps when building sitemaps for instance, where + * password protected posts are also excluded. * * @param string $post_type The post type. * - * @return object An object with last_modified and published_at timestamps. + * @return object An object with the number of posts, most recent last modified and first published at timestamps. */ - protected function get_object_timestamps( $post_type ) { + protected function get_public_post_archive_aggregates( $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 + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_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 = '' + WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) ) . ') AND p.post_type = %s - "; + AND p.post_password = "" + '; $replacements = \array_merge( $post_statuses, [ $post_type ] ); diff --git a/src/builders/indexable-system-page-builder.php b/src/builders/indexable-system-page-builder.php index a592ed414c0..c14e1a94d1e 100644 --- a/src/builders/indexable-system-page-builder.php +++ b/src/builders/indexable-system-page-builder.php @@ -20,7 +20,7 @@ class Indexable_System_Page_Builder { */ const OPTION_MAPPING = [ 'search-result' => [ - 'title' => 'title-search-wpseo', + 'title' => 'title-search-wpseo', ], '404' => [ 'title' => 'title-404-wpseo', @@ -65,11 +65,13 @@ public function __construct( * @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(); + $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->number_of_publicly_viewable_posts = 0; + $indexable->is_publicly_viewable = 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'] ); diff --git a/src/builders/indexable-term-builder.php b/src/builders/indexable-term-builder.php index 4f2ec43c217..2b8eec69467 100644 --- a/src/builders/indexable-term-builder.php +++ b/src/builders/indexable-term-builder.php @@ -8,7 +8,6 @@ use Yoast\WP\SEO\Helpers\Post_Helper; use Yoast\WP\SEO\Helpers\Taxonomy_Helper; use Yoast\WP\SEO\Models\Indexable; -use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions; /** @@ -98,11 +97,12 @@ public function build( $term_id, $indexable ) { $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->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->is_publicly_viewable = is_taxonomy_viewable( $term->taxonomy ); $indexable->primary_focus_keyword_score = $this->get_keyword_score( $this->get_meta_value( 'wpseo_focuskw', $term_meta ), @@ -110,7 +110,7 @@ public function build( $term_id, $indexable ) { ); $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; + $indexable->set_deprecated_property( 'is_public', ( $indexable->is_robots_noindex === null ) ? null : ! $indexable->is_robots_noindex ); $this->reset_social_images( $indexable ); @@ -132,15 +132,29 @@ public function build( $term_id, $indexable ) { $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 = $this->set_aggregate_values( $indexable ); $indexable->version = $this->version; return $indexable; } + /** + * Sets the aggregate values for a term indexable. + * + * @param Indexable $indexable The indexable to set the aggregates for. + * + * @return Indexable The indexable with set aggregates. + */ + public function set_aggregate_values( Indexable $indexable ) { + $aggregates = $this->get_public_post_archive_aggregates( $indexable->object_id, $indexable->object_sub_type ); + $indexable->object_published_at = $aggregates->first_published_at; + $indexable->object_last_modified = max( $indexable->object_last_modified, $aggregates->most_recent_last_modified ); + $indexable->number_of_publicly_viewable_posts = $aggregates->number_of_public_posts; + + return $indexable; + } + /** * Converts the meta noindex value to the indexable value. * @@ -241,18 +255,23 @@ protected function find_alternative_image( Indexable $indexable ) { } /** - * Returns the timestamps for a given term. + * Returns public post aggregates for a given term. + * We don't consider password protected posts to be public. This helps when building sitemaps for instance, where + * password protected posts are also excluded. * * @param int $term_id The term ID. * @param string $taxonomy The taxonomy. * - * @return object An object with last_modified and published_at timestamps. + * @return object An object with the number of posts, most recent last modified and first published at timestamps. */ - protected function get_object_timestamps( $term_id, $taxonomy ) { + protected function get_public_post_archive_aggregates( $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 + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$this->wpdb->posts} AS p INNER JOIN {$this->wpdb->term_relationships} AS term_rel ON term_rel.object_id = p.ID @@ -260,9 +279,9 @@ protected function get_object_timestamps( $term_id, $taxonomy ) { 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 = '' - "; + 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 ); diff --git a/src/config/migrations/20211105121254_ReplaceHasPublicPostsOnIndexables.php b/src/config/migrations/20211105121254_ReplaceHasPublicPostsOnIndexables.php new file mode 100644 index 00000000000..cecbf168c86 --- /dev/null +++ b/src/config/migrations/20211105121254_ReplaceHasPublicPostsOnIndexables.php @@ -0,0 +1,72 @@ +get_table_name(); + $this->rename_column( + $table_name, + 'has_public_posts', + 'number_of_publicly_viewable_posts' + ); + + $this->change_column( + $table_name, + 'number_of_publicly_viewable_posts', + 'integer' + ); + } + + /** + * Migration down. + * + * @return void + */ + public function down() { + $table_name = $this->get_table_name(); + $this->change_column( + $table_name, + 'number_of_publicly_viewable_posts', + 'boolean', + [ + 'null' => true, + 'default' => null, + ] + ); + + $this->rename_column( + $table_name, + 'number_of_publicly_viewable_posts', + 'has_public_posts' + ); + } + + /** + * Retrieves the table name to use. + * + * @return string The table name to use. + */ + protected function get_table_name() { + return Model::get_table_name( 'Indexable' ); + } +} diff --git a/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php b/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php new file mode 100644 index 00000000000..901a1db4fca --- /dev/null +++ b/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php @@ -0,0 +1,65 @@ +get_table_name(); + $this->add_column( + $table_name, + 'is_publicly_viewable', + 'boolean', + [ + 'null' => true, + 'default' => null, + ] + ); + } + + /** + * Migration down. + * Requires a reindex of indexables. + * + * @return void + */ + public function down() { + $table_name = $this->get_table_name(); + + $this->remove_column( + $table_name, + 'is_publicly_viewable' + ); + } + + /** + * Retrieves the table name to use. + * + * @return string The table name to use. + */ + protected function get_table_name() { + return Model::get_table_name( 'Indexable' ); + } +} + + + diff --git a/src/helpers/author-archive-helper.php b/src/helpers/author-archive-helper.php index 1f6bffed1f8..91c822a7a74 100644 --- a/src/helpers/author-archive-helper.php +++ b/src/helpers/author-archive-helper.php @@ -3,6 +3,7 @@ namespace Yoast\WP\SEO\Helpers; use Yoast\WP\Lib\Model; +use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * A helper object for author archives. @@ -15,12 +16,19 @@ class Author_Archive_Helper { * @return array The post types that are shown on an author's archive. */ public function get_author_archive_post_types() { + $default_post_types = [ 'post' ]; /** * Filters the array of post types that are shown on an author's archive. * * @param array $args The post types that are shown on an author archive. */ - return \apply_filters( 'wpseo_author_archive_post_types', [ 'post' ] ); + $post_types = \apply_filters( 'wpseo_author_archive_post_types', $default_post_types ); + + if ( ! is_array( $post_types ) ) { + return $default_post_types; + } + + return $post_types; } /** @@ -29,8 +37,12 @@ public function get_author_archive_post_types() { * @param int $author_id The author ID. * * @return bool|null Whether the author has at least one public post. + * + * @codeCoverageIgnore + * @deprecated 17.9 */ public function author_has_public_posts( $author_id ) { + \_deprecated_function( __METHOD__, '17.9', esc_html( Indexable_Repository::class ) . '::query_where_noindex' ); // First check if the author has at least one public post. $has_public_post = $this->author_has_a_public_post( $author_id ); if ( $has_public_post ) { @@ -54,6 +66,8 @@ public function author_has_public_posts( $author_id ) { * @param int $author_id The author ID. * * @return bool Whether the author has at least one public post. + * + * @deprecated 17.9 */ protected function author_has_a_public_post( $author_id ) { $cache_key = 'author_has_a_public_post_' . $author_id; @@ -85,6 +99,8 @@ protected function author_has_a_public_post( $author_id ) { * @param int $author_id The author ID. * * @return bool Whether the author has at least one post with the is public null. + * + * @deprecated 17.9 */ protected function author_has_a_post_with_is_public_null( $author_id ) { $cache_key = 'author_has_a_post_with_is_public_null_' . $author_id; diff --git a/src/helpers/post-helper.php b/src/helpers/post-helper.php index b3f3e7bb2e3..21c7fc8f56b 100644 --- a/src/helpers/post-helper.php +++ b/src/helpers/post-helper.php @@ -3,6 +3,7 @@ namespace Yoast\WP\SEO\Helpers; use WP_Post; +use Yoast\WP\SEO\Builders\Indexable_Builder; use Yoast\WP\SEO\Repositories\Indexable_Repository; /** @@ -24,6 +25,13 @@ class Post_Helper { */ private $repository; + /** + * A builder that creates and updates indexables. + * + * @var Indexable_Builder + */ + private $indexable_builder; + /** * Post_Helper constructor. * @@ -35,6 +43,21 @@ public function __construct( String_Helper $string ) { $this->string = $string; } + /** + * Sets the indexable builder. Done to avoid circular dependencies. + * + * @param Indexable_Builder $indexable_builder A builder that creates and updates indexables. + * + * @required + * + * @return void + * + * When the deprecated `$this->update_has_public_posts_on_attachments()` function is removed, this setter should also be removed. + */ + public function set_indexable_builder( Indexable_Builder $indexable_builder ) { + $this->indexable_builder = $indexable_builder; + } + /** * Sets the indexable repository. Done to avoid circular dependencies. * @@ -114,8 +137,10 @@ public function get_post( $post_id ) { return \get_post( $post_id ); } + // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Signature kept the same after deprecation. + /** - * Updates the has_public_posts field on attachments for a post_parent. + * Updates the number_of_publicly_viewable_posts field on attachments for a post_parent. * * An attachment is represented by their post parent when: * - The attachment has a post parent. @@ -123,39 +148,24 @@ public function get_post( $post_id ) { * * @codeCoverageIgnore It relies too much on dependencies. * - * @param int $post_parent Post ID. - * @param int $has_public_posts Whether the parent is public. + * @param int $post_parent Post ID. + * @param int $has_public_posts Unused. * * @return bool Whether the update was successful. + * + * @deprecated 17.9 + * When this function is removed, also remove the indexable_builder setter. */ public function update_has_public_posts_on_attachments( $post_parent, $has_public_posts ) { - $query = $this->repository->query() - ->select( 'id' ) - ->where( 'object_type', 'post' ) - ->where( 'object_sub_type', 'attachment' ) - ->where( 'post_status', 'inherit' ) - ->where( 'post_parent', $post_parent ); - - if ( $has_public_posts !== null ) { - $query->where_raw( '( has_public_posts IS NULL OR has_public_posts <> %s )', [ $has_public_posts ] ); - } - else { - $query->where_not_null( 'has_public_posts' ); - } - $results = $query->find_array(); + _deprecated_function( __METHOD__, '17.9' ); + $indexable = $this->repository->find_by_id_and_type( $post_parent, 'post' ); - if ( empty( $results ) ) { - return true; - } - - $updated = $this->repository->query() - ->set( 'has_public_posts', $has_public_posts ) - ->where_id_in( \wp_list_pluck( $results, 'id' ) ) - ->update_many(); - - return $updated !== false; + $this->indexable_builder->recalculate_aggregates( $indexable ); + return true; } + // phpcs:enable + /** * Determines if the post can be indexed. * diff --git a/src/helpers/post-type-helper.php b/src/helpers/post-type-helper.php index aaf6d838093..5201b4b260b 100644 --- a/src/helpers/post-type-helper.php +++ b/src/helpers/post-type-helper.php @@ -79,6 +79,30 @@ public function get_accessible_post_types() { return $post_types; } + /** + * Checks whether a post_type has a publicly viewable archive. + * + * @param string $post_type The name of a registered post type. + * + * @return bool Whether a post_type has a publicly viewable archive. + */ + public function has_publicly_viewable_archive( $post_type ) { + $post_type_object = get_post_type_object( $post_type ); + if ( $post_type_object === null ) { + return false; + } + + if ( $post_type_object->publicly_queryable === false ) { + return false; + } + + if ( $post_type_object->rewrite === false ) { + return false; + } + + return $post_type_object->has_archive; + } + /** * Returns an array of post types that are excluded from being indexed for the * indexables. diff --git a/src/helpers/robots-helper.php b/src/helpers/robots-helper.php index 94a8bd3e578..d8e2da3493b 100644 --- a/src/helpers/robots-helper.php +++ b/src/helpers/robots-helper.php @@ -7,6 +7,22 @@ */ class Robots_Helper { + /** + * A helper to get and set plugin options. + * + * @var Options_Helper + */ + private $options_helper; + + /** + * Robots_Helper constructor. + * + * @param Options_Helper $options_helper A helper to get and set plugin options. + */ + public function __construct( Options_Helper $options_helper ) { + $this->options_helper = $options_helper; + } + /** * Sets the robots index to noindex. * @@ -24,4 +40,25 @@ public function set_robots_no_index( $robots ) { return $robots; } + + /** + * Gets the site default noindex value for an object type. + * + * @param string $object_type The object type. + * @param string $object_sub_type The object subtype. Used for post_types. + * + * @return bool Whether the site default is set to noindex for the requested object type. + */ + public function get_default_noindex_for_object( $object_type, $object_sub_type = '' ) { + switch ( $object_type ) { + case 'post': + return (bool) $this->options_helper->get( 'noindex-' . $object_sub_type ); + case 'user': + return (bool) $this->options_helper->get( 'noindex-author-wpseo' ); + case 'term': + return (bool) $this->options_helper->get( 'noindex-tax-' . $object_sub_type ); + default: + return false; + } + } } diff --git a/src/integrations/watchers/indexable-date-archive-watcher.php b/src/integrations/watchers/indexable-date-archive-watcher.php index 9b77cb1fc07..b3367e55ac8 100644 --- a/src/integrations/watchers/indexable-date-archive-watcher.php +++ b/src/integrations/watchers/indexable-date-archive-watcher.php @@ -66,7 +66,13 @@ public function register_hooks() { * @return void */ public function check_option( $old_value, $new_value ) { - $relevant_keys = [ 'title-archive-wpseo', 'breadcrumbs-archiveprefix', 'metadesc-archive-wpseo', 'noindex-archive-wpseo' ]; + $relevant_keys = [ + 'title-archive-wpseo', + 'breadcrumbs-archiveprefix', + 'metadesc-archive-wpseo', + 'noindex-archive-wpseo', + 'disable-date', + ]; foreach ( $relevant_keys as $key ) { // If both values aren't set they haven't changed. diff --git a/src/integrations/watchers/indexable-post-watcher.php b/src/integrations/watchers/indexable-post-watcher.php index bf017926a49..56513f59a6a 100644 --- a/src/integrations/watchers/indexable-post-watcher.php +++ b/src/integrations/watchers/indexable-post-watcher.php @@ -119,7 +119,7 @@ public function __construct( */ public function register_hooks() { \add_action( 'wp_insert_post', [ $this, 'build_indexable' ], \PHP_INT_MAX ); - \add_action( 'delete_post', [ $this, 'delete_indexable' ] ); + \add_action( 'delete_post', [ $this, 'delete_indexable' ], 10, 2 ); \add_action( 'edit_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX ); \add_action( 'add_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX ); @@ -129,11 +129,12 @@ public function register_hooks() { /** * Deletes the meta when a post is deleted. * - * @param int $post_id Post ID. + * @param int $post_id Post ID. + * @param \WP_Post $post The to be deleted post. * * @return void */ - public function delete_indexable( $post_id ) { + public function delete_indexable( $post_id, $post ) { $indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false ); // Only interested in post indexables. @@ -141,13 +142,10 @@ public function delete_indexable( $post_id ) { return; } - $this->update_relations( $this->post->get_post( $post_id ) ); - - $this->update_has_public_posts( $indexable ); - $this->hierarchy_repository->clear_ancestors( $indexable->id ); $this->link_builder->delete( $indexable ); $indexable->delete(); + $this->update_relations( $post ); } /** @@ -167,10 +165,8 @@ public function updated_indexable( $indexable, $post ) { $post = $this->post->get_post( $indexable->object_id ); } - $this->update_relations( $post ); - $this->update_has_public_posts( $indexable ); - $indexable->save(); + $this->update_relations( $post ); } /** @@ -197,32 +193,13 @@ public function build_indexable( $post_id ) { $this->link_builder->build( $indexable, $post->post_content ); // Save indexable to persist the updated link count. $indexable->save(); - $this->updated_indexable( $indexable, $post ); } + $this->updated_indexable( $indexable, $post ); } catch ( Exception $exception ) { $this->logger->log( LogLevel::ERROR, $exception->getMessage() ); } } - /** - * Updates the has_public_posts when the post indexable is built. - * - * @param Indexable $indexable The indexable to check. - */ - protected function update_has_public_posts( $indexable ) { - // Update the author indexable's has public posts value. - try { - $author_indexable = $this->repository->find_by_id_and_type( $indexable->author_id, 'user' ); - $author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id ); - $author_indexable->save(); - } catch ( Exception $exception ) { - $this->logger->log( LogLevel::ERROR, $exception->getMessage() ); - } - - // Update possible attachment's has public posts value. - $this->post->update_has_public_posts_on_attachments( $indexable->object_id, $indexable->is_public ); - } - /** * Updates the relations on post save or post status change. * @@ -230,10 +207,10 @@ protected function update_has_public_posts( $indexable ) { */ protected function update_relations( $post ) { $related_indexables = $this->get_related_indexables( $post ); - - foreach ( $related_indexables as $indexable ) { - $indexable->object_last_modified = max( $indexable->object_last_modified, $post->post_modified_gmt ); - $indexable->save(); + $now = current_time( 'mysql' ); + foreach ( $related_indexables as $related_indexable ) { + $related_indexable->object_last_modified = $now; + $this->builder->recalculate_aggregates( $related_indexable ); } } @@ -254,12 +231,27 @@ protected function get_related_indexables( $post ) { $related_indexables[] = $this->repository->find_by_id_and_type( $post->post_author, 'user', false ); $related_indexables[] = $this->repository->find_for_post_type_archive( $post->post_type, false ); $related_indexables[] = $this->repository->find_for_home_page( false ); + $related_indexables = \array_merge( + $related_indexables, + $this->get_related_term_indexables( $post->ID ) + ); + + return \array_filter( $related_indexables ); + } - $taxonomies = \get_post_taxonomies( $post->ID ); + /** + * Retrieves the related term indexables for a given post. + * + * @param int $post_id The id of the post to get the related term indexables for. + * + * @return Indexable[] The term indexables related to the post. + */ + protected function get_related_term_indexables( $post_id ) { + $taxonomies = \get_post_taxonomies( $post_id ); $taxonomies = \array_filter( $taxonomies, 'is_taxonomy_viewable' ); $term_ids = []; foreach ( $taxonomies as $taxonomy ) { - $terms = \get_the_terms( $post->ID, $taxonomy ); + $terms = \get_the_terms( $post_id, $taxonomy ); if ( empty( $terms ) || \is_wp_error( $terms ) ) { continue; @@ -267,12 +259,8 @@ protected function get_related_indexables( $post ) { $term_ids = \array_merge( $term_ids, \wp_list_pluck( $terms, 'term_id' ) ); } - $related_indexables = \array_merge( - $related_indexables, - $this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false ) - ); - return \array_filter( $related_indexables ); + return $this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false ); } /** diff --git a/src/models/indexable.php b/src/models/indexable.php index 06f9f7696d6..8fd7410bbad 100644 --- a/src/models/indexable.php +++ b/src/models/indexable.php @@ -59,10 +59,11 @@ * * @property int $prominent_words_version * - * @property bool $is_public * @property bool $is_protected * @property string $post_status - * @property bool $has_public_posts + * + * @property int $number_of_publicly_viewable_posts + * @property bool $is_publicly_viewable * * @property int $blog_id * @@ -109,8 +110,9 @@ class Indexable extends Model { 'is_robots_noimageindex', 'is_robots_nosnippet', 'is_cornerstone', - 'is_public', + 'is_publicly_viewable', 'is_protected', + 'is_public', 'has_public_posts', ]; @@ -133,6 +135,25 @@ class Indexable extends Model { 'blog_id', 'estimated_reading_time_minutes', 'version', + 'number_of_publicly_viewable_posts', + ]; + + /** + * Which columns are deprecated. + * + * @var array + */ + protected $deprecated_columns = [ + 'has_public_posts' => [ + 'since' => '17.9', + 'replacement' => 'number_of_publicly_viewable_posts', + 'automatically_use_replacement' => true, + ], + 'is_public' => [ + 'since' => '17.9', + 'replacement' => 'is_publicly_viewable', + 'automatically_use_replacement' => false, + ], ]; /** diff --git a/src/repositories/indexable-repository.php b/src/repositories/indexable-repository.php index 83221f1db6b..a54404625ee 100644 --- a/src/repositories/indexable-repository.php +++ b/src/repositories/indexable-repository.php @@ -9,6 +9,7 @@ use Yoast\WP\SEO\Builders\Indexable_Builder; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Indexable_Helper; +use Yoast\WP\SEO\Helpers\Robots_Helper; use Yoast\WP\SEO\Loggers\Logger; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager; @@ -37,7 +38,7 @@ class Indexable_Repository { * * @var Current_Page_Helper */ - protected $current_page; + protected $current_page_helper; /** * The logger object. @@ -67,30 +68,40 @@ class Indexable_Repository { */ protected $version_manager; + /** + * A helper class for robots values. + * + * @var Robots_Helper + */ + private $robots_helper; + /** * Returns the instance of this class constructed through the ORM Wrapper. * * @param Indexable_Builder $builder The indexable builder. - * @param Current_Page_Helper $current_page The current post helper. + * @param Current_Page_Helper $current_page_helper The current post helper. * @param Logger $logger The logger. * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository. * @param wpdb $wpdb The WordPress database instance. * @param Indexable_Version_Manager $version_manager The indexable version manager. + * @param Robots_Helper $robots_helper A helper class for robots values. */ public function __construct( Indexable_Builder $builder, - Current_Page_Helper $current_page, + Current_Page_Helper $current_page_helper, Logger $logger, Indexable_Hierarchy_Repository $hierarchy_repository, wpdb $wpdb, - Indexable_Version_Manager $version_manager + Indexable_Version_Manager $version_manager, + Robots_Helper $robots_helper ) { $this->builder = $builder; - $this->current_page = $current_page; + $this->current_page_helper = $current_page_helper; $this->logger = $logger; $this->hierarchy_repository = $hierarchy_repository; $this->wpdb = $wpdb; $this->version_manager = $version_manager; + $this->robots_helper = $robots_helper; } /** @@ -113,31 +124,31 @@ public function for_current_page() { $indexable = false; switch ( true ) { - case $this->current_page->is_simple_page(): - $indexable = $this->find_by_id_and_type( $this->current_page->get_simple_page_id(), 'post' ); + case $this->current_page_helper->is_simple_page(): + $indexable = $this->find_by_id_and_type( $this->current_page_helper->get_simple_page_id(), 'post' ); break; - case $this->current_page->is_home_static_page(): - $indexable = $this->find_by_id_and_type( $this->current_page->get_front_page_id(), 'post' ); + case $this->current_page_helper->is_home_static_page(): + $indexable = $this->find_by_id_and_type( $this->current_page_helper->get_front_page_id(), 'post' ); break; - case $this->current_page->is_home_posts_page(): + case $this->current_page_helper->is_home_posts_page(): $indexable = $this->find_for_home_page(); break; - case $this->current_page->is_term_archive(): - $indexable = $this->find_by_id_and_type( $this->current_page->get_term_id(), 'term' ); + case $this->current_page_helper->is_term_archive(): + $indexable = $this->find_by_id_and_type( $this->current_page_helper->get_term_id(), 'term' ); break; - case $this->current_page->is_date_archive(): + case $this->current_page_helper->is_date_archive(): $indexable = $this->find_for_date_archive(); break; - case $this->current_page->is_search_result(): + case $this->current_page_helper->is_search_result(): $indexable = $this->find_for_system_page( 'search-result' ); break; - case $this->current_page->is_post_type_archive(): - $indexable = $this->find_for_post_type_archive( $this->current_page->get_queried_post_type() ); + case $this->current_page_helper->is_post_type_archive(): + $indexable = $this->find_for_post_type_archive( $this->current_page_helper->get_queried_post_type() ); break; - case $this->current_page->is_author_archive(): - $indexable = $this->find_by_id_and_type( $this->current_page->get_author_id(), 'user' ); + case $this->current_page_helper->is_author_archive(): + $indexable = $this->find_by_id_and_type( $this->current_page_helper->get_author_id(), 'user' ); break; - case $this->current_page->is_404(): + case $this->current_page_helper->is_404(): $indexable = $this->find_for_system_page( '404' ); break; } @@ -155,6 +166,48 @@ public function for_current_page() { return $indexable; } + /** + * Gets a query that finds all indexables of a type+subtype that match the noindex value + * while taking site defaults into account. + * + * @param bool $noindex The noindex value of the posts to find. + * @param string $object_type The indexable object type. + * @param string|null $object_sub_type The indexable object subtype. + * @param boolean $noindex_empty_archives Whether an archive should be considered as noindex if it has no public posts. + * + * @return ORM The query object. + */ + public function query_where_noindex( $noindex, $object_type, $object_sub_type = null, $noindex_empty_archives = true ) { + $query = $this + ->query() + ->where( 'object_type', $object_type ); + + if ( $object_sub_type !== null ) { + $query->where( 'object_sub_type', $object_sub_type ); + } + + $default_noindex = $this->robots_helper->get_default_noindex_for_object( $object_type, $object_sub_type ); + + $condition = 'is_robots_noindex = %d '; + // If the requested noindex value matches the default, include NULL values in the result. + if ( $default_noindex === $noindex ) { + $condition = '(' . $condition . 'OR is_robots_noindex IS NULL )'; + } + + // Let the number of posts in an archive determine the noindex value. + $is_archive_type = in_array( $object_type, [ 'post-type-archive', 'term', 'user', 'home-page' ], true ); + if ( $is_archive_type && $noindex_empty_archives ) { + if ( $noindex === true ) { + $condition = '(' . $condition . ') OR number_of_publicly_viewable_posts = 0'; + } + else { + $condition = '(' . $condition . ') AND number_of_publicly_viewable_posts > 0'; + } + } + + return $query->where_raw( $condition, (int) $noindex ); + } + /** * Retrieves an indexable by its permalink. * @@ -166,7 +219,8 @@ public function find_by_permalink( $permalink ) { $permalink_hash = \strlen( $permalink ) . ':' . \md5( $permalink ); // Find by both permalink_hash and permalink, permalink_hash is indexed so will be used first by the DB to optimize the query. - return $this->query() + return $this + ->query() ->where( 'permalink_hash', $permalink_hash ) ->where( 'permalink', $permalink ) ->find_one(); @@ -281,7 +335,8 @@ public function find_for_post_type_archive( $post_type, $auto_create = true ) { * * @var Indexable $indexable */ - $indexable = $this->query() + $indexable = $this + ->query() ->where( 'object_type', 'post-type-archive' ) ->where( 'object_sub_type', $post_type ) ->find_one(); @@ -307,7 +362,8 @@ public function find_for_system_page( $object_sub_type, $auto_create = true ) { * * @var Indexable $indexable */ - $indexable = $this->query() + $indexable = $this + ->query() ->where( 'object_type', 'system-page' ) ->where( 'object_sub_type', $object_sub_type ) ->find_one(); @@ -329,7 +385,8 @@ public function find_for_system_page( $object_sub_type, $auto_create = true ) { * @return bool|Indexable Instance of indexable. */ public function find_by_id_and_type( $object_id, $object_type, $auto_create = true ) { - $indexable = $this->query() + $indexable = $this + ->query() ->where( 'object_id', $object_id ) ->where( 'object_type', $object_type ) ->find_one(); @@ -363,7 +420,8 @@ public function find_by_multiple_ids_and_type( $object_ids, $object_type, $auto_ * * @var Indexable[] $indexables */ - $indexables = $this->query() + $indexables = $this + ->query() ->where_in( 'object_id', $object_ids ) ->where( 'object_type', $object_type ) ->find_many(); @@ -432,7 +490,8 @@ public function get_ancestors( Indexable $indexable ) { return []; } - $indexables = $this->query() + $indexables = $this + ->query() ->where_in( 'id', $indexable_ids ) ->order_by_expr( 'FIELD(id,' . \implode( ',', $indexable_ids ) . ')' ) ->find_many(); @@ -449,7 +508,8 @@ public function get_ancestors( Indexable $indexable ) { * @return Indexable[] array of indexables. */ public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) { - $query = $this->query() + $query = $this + ->query() ->where( 'post_parent', $post_parent ) ->where( 'object_type', 'post' ) ->where( 'post_status', 'publish' ); @@ -457,6 +517,7 @@ public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) { if ( ! empty( $exclude_ids ) ) { $query->where_not_in( 'object_id', $exclude_ids ); } + return $query->find_many(); } @@ -469,7 +530,8 @@ public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) { * @return bool Whether or not the update was succeful. */ public function update_incoming_link_count( $indexable_id, $count ) { - return (bool) $this->query() + return (bool) $this + ->query() ->set( 'incoming_link_count', $count ) ->where( 'id', $indexable_id ) ->update_many(); @@ -505,6 +567,7 @@ public function upgrade_indexable( $indexable ) { if ( $this->version_manager->indexable_needs_upgrade( $indexable ) ) { $indexable = $this->builder->build( $indexable ); } + return $indexable; } diff --git a/src/values/indexables/indexable-builder-versions.php b/src/values/indexables/indexable-builder-versions.php index 4f327dd88da..799c23cbd70 100644 --- a/src/values/indexables/indexable-builder-versions.php +++ b/src/values/indexables/indexable-builder-versions.php @@ -14,17 +14,22 @@ class Indexable_Builder_Versions { * If the key is not in this list, the indexable type will not be managed. * These numbers should be increased if one of the builders implements a new feature. * + * When you change the version of the indexable builder, change the plugin version number of the comment too. + * This is to prevent the same builder version to be increased to the same version in multiple PRs across multiple releases. + * When 2 PRs change the same builder version for the same release, it won't cause merge conflicts - which is OK, as it is the same release + * But if the same version number is used for two separate releases, the later release should use a higher builder version number. + * * @var array */ protected $indexable_builder_versions_by_type = [ - 'date-archive' => self::DEFAULT_INDEXABLE_BUILDER_VERSION, - 'general' => self::DEFAULT_INDEXABLE_BUILDER_VERSION, - 'home-page' => 2, - 'post' => 2, - 'post-type-archive' => 2, - 'term' => 2, - 'user' => 2, - 'system-page' => self::DEFAULT_INDEXABLE_BUILDER_VERSION, + 'date-archive' => 2, // Since 17.9. + 'general' => 2, // Since 17.9. + 'home-page' => 3, // Since 17.9. + 'post' => 3, // Since 17.9. + 'post-type-archive' => 3, // Since 17.9. + 'term' => 3, // Since 17.9. + 'user' => 3, // Since 17.9. + 'system-page' => 2, // Since 17.9. ]; /** diff --git a/tests/unit/builders/indexable-author-builder-test.php b/tests/unit/builders/indexable-author-builder-test.php index 07b2622849f..2febb5a6dc7 100644 --- a/tests/unit/builders/indexable-author-builder-test.php +++ b/tests/unit/builders/indexable-author-builder-test.php @@ -15,8 +15,8 @@ /** * Class Indexable_Author_Test. * - * @group indexables - * @group builders + * @group indexables + * @group builders * * @coversDefaultClass \Yoast\WP\SEO\Builders\Indexable_Author_Builder * @covers \Yoast\WP\SEO\Builders\Indexable_Author_Builder @@ -54,7 +54,7 @@ class Indexable_Author_Builder_Test extends TestCase { /** * The wpdb instance * - * @var wpdb|Mockery\MockInterface + * @var \wpdb|Mockery\MockInterface */ protected $wpdb; @@ -94,8 +94,7 @@ protected function set_up() { $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image_id', null ); $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image_source', null ); - $this->indexable_mock->orm->expects( 'set' )->with( 'is_public', null ); - $this->indexable_mock->orm->expects( 'set' )->with( 'has_public_posts', true ); + $this->indexable_mock->expects( 'set_deprecated_property' )->with( 'is_public', null ); $this->indexable_mock->orm->expects( 'get' )->with( 'is_robots_noindex' )->andReturn( 0 ); @@ -104,9 +103,8 @@ protected function set_up() { $this->author_archive = Mockery::mock( Author_Archive_Helper::class ); $this->author_archive - ->expects( 'author_has_public_posts' ) - ->with( 1 ) - ->andReturn( true ); + ->expects( 'get_author_archive_post_types' ) + ->andReturn( [ 'post', 'my-cpt' ] ); $this->versions = Mockery::mock( Indexable_Builder_Versions::class ); $this->versions @@ -115,9 +113,32 @@ protected function set_up() { ->andReturn( 2 ); $this->post_helper = Mockery::mock( Post_Helper::class ); + $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); + $this->wpdb = Mockery::mock( 'wpdb' ); $this->wpdb->posts = 'wp_posts'; + $this->wpdb->expects( 'prepare' )->with( + " + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at + FROM {$this->wpdb->posts} AS p + WHERE p.post_status IN (%s) + AND p.post_author = %d + AND p.post_type IN (%s, %s) + ", + [ 'publish', 1, 'post', 'my-cpt' ] + )->andReturn( 'PREPARED_QUERY' ); + $this->wpdb->expects( 'get_row' )->with( 'PREPARED_QUERY' )->andReturn( + (object) [ + 'number_of_public_posts' => '7', + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', + ] + ); + $this->instance = new Indexable_Author_Builder( $this->author_archive, $this->versions, $this->post_helper, $this->wpdb ); } @@ -137,39 +158,22 @@ public function test_build() { $this->indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', true ); $this->indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); + $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'object_id' )->andReturn( 1 ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'open_graph_image' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'open_graph_image_id' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'open_graph_image_source' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'twitter_image' ); $this->indexable_mock->orm->expects( 'get' )->times( 3 )->with( 'twitter_image_id' ); - $this->indexable_mock->orm->expects( 'get' )->with( 'object_id' ); + $this->indexable_mock->orm->expects( 'get' )->once()->with( 'object_last_modified' ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image', 'avatar_image.jpg' ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image_source', 'gravatar-image' ); $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image', 'avatar_image.jpg' ); $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image_source', 'gravatar-image' ); - $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); - - $this->wpdb->expects( 'prepare' )->once()->with( - " - 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 (%s) - AND p.post_password = '' - AND p.post_author = %d - ", - [ 'publish', 1 ] - )->andReturn( 'PREPARED_QUERY' ); - $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( - (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', - ] - ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', '7' ); Monkey\Functions\expect( 'get_avatar_url' ) ->once() @@ -193,39 +197,22 @@ public function test_build_without_alternative_image() { $this->indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', true ); $this->indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); + $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'object_id' )->andReturn( 1 ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'open_graph_image' ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'open_graph_image_id' ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'open_graph_image_source' ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'twitter_image' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'twitter_image_id' ); - $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'object_id' ); - - $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); - - $this->wpdb->expects( 'prepare' )->once()->with( - " - 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 (%s) - AND p.post_password = '' - AND p.post_author = %d - ", - [ 'publish', 1 ] - )->andReturn( 'PREPARED_QUERY' ); - $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( - (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', - ] - ); + $this->indexable_mock->orm->expects( 'get' )->once()->with( 'object_last_modified' ); $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', '7' ); Monkey\Functions\expect( 'get_avatar_url' ) ->once() ->with( - $this->indexable_mock->object_id, + 1, [ 'size' => 500, 'scheme' => 'https', @@ -252,39 +239,22 @@ public function test_build_with_undefined_author_meta() { $this->indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', false ); $this->indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); + $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'object_id' )->andReturn( 1 ); $this->indexable_mock->orm->expects( 'get' )->once()->with( 'open_graph_image' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'open_graph_image_id' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'open_graph_image_source' ); $this->indexable_mock->orm->expects( 'get' )->twice()->with( 'twitter_image' ); $this->indexable_mock->orm->expects( 'get' )->times( 3 )->with( 'twitter_image_id' ); - $this->indexable_mock->orm->expects( 'get' )->with( 'object_id' ); + $this->indexable_mock->orm->expects( 'get' )->once()->with( 'object_last_modified' ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image', 'avatar_image.jpg' ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image_source', 'gravatar-image' ); $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image', 'avatar_image.jpg' ); $this->indexable_mock->orm->expects( 'set' )->with( 'twitter_image_source', 'gravatar-image' ); - $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); - - $this->wpdb->expects( 'prepare' )->once()->with( - " - 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 (%s) - AND p.post_password = '' - AND p.post_author = %d - ", - [ 'publish', 1 ] - )->andReturn( 'PREPARED_QUERY' ); - $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( - (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', - ] - ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', '7' ); Monkey\Functions\expect( 'get_avatar_url' ) ->once() diff --git a/tests/unit/builders/indexable-date-archive-builder-test.php b/tests/unit/builders/indexable-date-archive-builder-test.php index f3aa8b4ab7e..591fa7fd014 100644 --- a/tests/unit/builders/indexable-date-archive-builder-test.php +++ b/tests/unit/builders/indexable-date-archive-builder-test.php @@ -34,20 +34,22 @@ public function test_build() { $options_mock->expects( 'get' )->with( 'title-archive-wpseo' )->andReturn( 'date_archive_title' ); $options_mock->expects( 'get' )->with( 'metadesc-archive-wpseo' )->andReturn( 'date_archive_meta_description' ); $options_mock->expects( 'get' )->with( 'noindex-archive-wpseo' )->andReturn( false ); + $options_mock->expects( 'get' )->with( 'disable-date' )->andReturn( false ); $indexable_mock = Mockery::mock( Indexable::class ); $indexable_mock->orm = Mockery::mock( ORM::class ); $indexable_mock->orm->expects( 'set' )->with( 'object_type', 'date-archive' ); $indexable_mock->orm->expects( 'set' )->with( 'title', 'date_archive_title' ); $indexable_mock->orm->expects( 'set' )->with( 'description', 'date_archive_meta_description' ); - $indexable_mock->orm->expects( 'set' )->with( 'is_public', true ); $indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', false ); $indexable_mock->orm->expects( 'get' )->with( 'is_robots_noindex' )->andReturn( 0 ); + $indexable_mock->expects( 'set_deprecated_property' )->with( 'is_public', true ); Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); $indexable_mock->orm->expects( 'set' )->with( 'permalink', null ); - $indexable_mock->orm->expects( 'set' )->with( 'version', 1 ); + $indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); + $indexable_mock->orm->expects( 'set' )->with( 'is_publicly_viewable', 1 ); $builder = new Indexable_Date_Archive_Builder( $options_mock, new Indexable_Builder_Versions() ); $builder->build( $indexable_mock ); diff --git a/tests/unit/builders/indexable-home-page-builder-test.php b/tests/unit/builders/indexable-home-page-builder-test.php index 04258ca5c01..876f27ed0da 100644 --- a/tests/unit/builders/indexable-home-page-builder-test.php +++ b/tests/unit/builders/indexable-home-page-builder-test.php @@ -4,6 +4,7 @@ use Brain\Monkey; use Mockery; +use wpdb; use WPSEO_Utils; use Yoast\WP\Lib\ORM; use Yoast\WP\SEO\Builders\Indexable_Home_Page_Builder; @@ -20,13 +21,13 @@ /** * Class Indexable_Author_Test. * - * @group indexables - * @group builders + * @group indexables + * @group builders * * @coversDefaultClass \Yoast\WP\SEO\Builders\Indexable_Author_Builder * @covers \Yoast\WP\SEO\Builders\Indexable_Home_Page_Builder * - * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- 5 words is fine. + * @phpcs :disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded -- 5 words is fine. */ class Indexable_Home_Page_Builder_Test extends TestCase { @@ -142,13 +143,13 @@ protected function set_up() { $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_description', 'home_og_description' ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image_source', null ); $this->indexable_mock->orm->expects( 'set' )->with( 'open_graph_image_meta', null ); + $this->indexable_mock->orm->expects( 'set' )->with( 'is_publicly_viewable', 1 ); $this->indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); // Mock offsetExists. $this->indexable_mock->orm->expects( 'offsetExists' )->with( 'description' )->andReturn( true ); // Mock Indexable ORM getters. - $this->indexable_mock->orm->expects( 'get' )->with( 'description' )->andReturn( 'home_meta_description' ); $this->indexable_mock->orm->expects( 'get' )->with( 'open_graph_image' )->andReturn( 'home_og_image' ); $this->indexable_mock->orm->expects( 'get' )->with( 'open_graph_image_id' )->andReturn( 1337 )->twice(); @@ -200,6 +201,7 @@ public function test_build() { $this->options_mock->expects( 'get' )->with( 'metadesc-home-wpseo' )->andReturn( 'home_meta_description' ); $this->indexable_mock->orm->expects( 'set' )->with( 'description', 'home_meta_description' ); + $this->indexable_mock->orm->expects( 'get' )->with( 'description' )->andReturn( 'home_meta_description' ); Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $this->indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); @@ -208,23 +210,28 @@ public function test_build() { $this->wpdb->expects( 'prepare' )->once()->with( " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$this->wpdb->posts} AS p WHERE p.post_status IN (%s) - AND p.post_password = '' AND p.post_type = 'post' ", [ 'publish' ] )->andReturn( 'PREPARED_QUERY' ); $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', + 'number_of_public_posts' => 20, + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', ] ); + $this->indexable_mock->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-01-01 00:00:00' ); $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 20 ); $this->instance->build( $this->indexable_mock ); } @@ -242,8 +249,12 @@ public function test_build_with_fallback_description() { // When no meta description is stored in the WP_Options... $this->options_mock->expects( 'get' )->with( 'metadesc-home-wpseo' )->andReturn( false ); + // We expect the description to be `false` in the ORM layer. - $this->indexable_mock->orm->expects( 'set' )->with( 'description', false ); + $this->indexable_mock->orm->expects( 'set' )->once()->with( 'description', false ); + $this->indexable_mock->orm->expects( 'get' )->with( 'description' )->andReturn( false ); + // Brainmonkey makes get_bloginfo( 'description' ) return 'description'. + $this->indexable_mock->orm->expects( 'set' )->once()->with( 'description', 'description' ); Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $this->indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); @@ -252,23 +263,28 @@ public function test_build_with_fallback_description() { $this->wpdb->expects( 'prepare' )->once()->with( " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$this->wpdb->posts} AS p WHERE p.post_status IN (%s) - AND p.post_password = '' AND p.post_type = 'post' ", [ 'publish' ] )->andReturn( 'PREPARED_QUERY' ); $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', + 'number_of_public_posts' => 20, + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', ] ); + $this->indexable_mock->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-01-01 00:00:00' ); $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 20 ); $this->instance->build( $this->indexable_mock ); } @@ -280,6 +296,7 @@ public function test_build_open_graph_image_meta_data() { $this->options_mock->expects( 'get' )->with( 'metadesc-home-wpseo' )->andReturn( 'home_meta_description' ); $this->indexable_mock->orm->expects( 'set' )->with( 'description', 'home_meta_description' ); + $this->indexable_mock->orm->expects( 'get' )->with( 'description' )->andReturn( 'home_meta_description' ); // Transform the image meta mock to JSON, since we expect that to be stored in the DB. $image_meta_mock_json = WPSEO_Utils::format_json_encode( $this->image_meta_mock ); @@ -295,23 +312,28 @@ public function test_build_open_graph_image_meta_data() { $this->wpdb->expects( 'prepare' )->once()->with( " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$this->wpdb->posts} AS p WHERE p.post_status IN (%s) - AND p.post_password = '' AND p.post_type = 'post' ", [ 'publish' ] )->andReturn( 'PREPARED_QUERY' ); $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', + 'number_of_public_posts' => 20, + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', ] ); + $this->indexable_mock->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-01-01 00:00:00' ); $this->indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $this->indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 20 ); $this->instance->build( $this->indexable_mock ); } diff --git a/tests/unit/builders/indexable-post-builder-test.php b/tests/unit/builders/indexable-post-builder-test.php index 0a49c4f317b..9cc7db6ba5b 100644 --- a/tests/unit/builders/indexable-post-builder-test.php +++ b/tests/unit/builders/indexable-post-builder-test.php @@ -69,7 +69,7 @@ class Indexable_Post_Builder_Test extends TestCase { * * @var Post_Helper|Mockery\MockInterface */ - protected $post; + protected $post_helper; /** * The post type helper. @@ -98,11 +98,11 @@ protected function set_up() { $this->image = Mockery::mock( Image_Helper::class ); $this->open_graph_image = Mockery::mock( Open_Graph_Image_Helper::class ); $this->twitter_image = Mockery::mock( Twitter_Image_Helper::class ); - $this->post = Mockery::mock( Post_Helper::class ); + $this->post_helper = Mockery::mock( Post_Helper::class ); $this->post_type_helper = Mockery::mock( Post_Type_Helper::class ); $this->instance = new Indexable_Post_Builder_Double( - $this->post, + $this->post_helper, $this->post_type_helper, new Indexable_Builder_Versions() ); @@ -131,19 +131,23 @@ protected function set_indexable_set_expectations( $indexable_mock, $expectation * Mocks a Twitter image that has been set by the user. */ protected function twitter_image_set_by_user() { - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image' ) ->andReturn( 'twitter-image' ); - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image_id' ) ->andReturn( 'twitter-image-id' ); - $this->twitter_image->shouldReceive( 'get_by_id' ) + $this->twitter_image + ->shouldReceive( 'get_by_id' ) ->with( 'twitter-image-id' ) ->andReturn( 'twitter_image' ); - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image_source' ) ->andReturn( 'set-by-user' ); } @@ -154,20 +158,24 @@ protected function twitter_image_set_by_user() { * @param array $image_meta The mocked meta data of the image. */ protected function open_graph_image_set_by_user( $image_meta ) { - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->with( 'open_graph_image' ) ->andReturn( 'open-graph-image' ); - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->twice() ->with( 'open_graph_image_id' ) ->andReturn( 'open-graph-image-id' ); - $this->indexable->orm->shouldReceive( 'get' ) + $this->indexable->orm + ->shouldReceive( 'get' ) ->with( 'open_graph_image_source' ) ->andReturn( 'set-by-user' ); - $this->open_graph_image->shouldReceive( 'get_image_by_id' ) + $this->open_graph_image + ->shouldReceive( 'get_image_by_id' ) ->with( 'open-graph-image-id' ) ->andReturn( $image_meta ); } @@ -235,7 +243,8 @@ public function test_build() { ); Monkey\Functions\expect( 'maybe_unserialize' )->andReturnFirstArg(); - $this->post->expects( 'get_post' ) + $this->post_helper + ->expects( 'get_post' ) ->once() ->with( 1 ) ->andReturn( @@ -256,57 +265,58 @@ public function test_build() { ->with( 'post' ) ->andReturn( false ); - $this->post + $this->post_helper ->expects( 'is_post_indexable' ) ->with( 1 ) ->andReturn( true ); $indexable_expectations = [ - 'object_id' => 1, - 'object_type' => 'post', - 'object_sub_type' => 'post', - 'permalink' => 'https://permalink', - 'canonical' => 'https://canonical', - 'title' => 'title', - 'breadcrumb_title' => 'breadcrumb_title', - 'description' => 'description', - 'open_graph_title' => 'open_graph_title', - 'open_graph_image' => 'open_graph_image', - 'open_graph_image_id' => 'open_graph_image_id', - 'open_graph_description' => 'open_graph_description', - 'twitter_title' => 'twitter_title', - 'twitter_image' => 'twitter_image', - 'twitter_image_id' => null, - 'twitter_description' => 'twitter_description', - 'is_cornerstone' => true, - 'is_robots_noindex' => true, - 'is_robots_nofollow' => true, - 'is_robots_noarchive' => false, - 'is_robots_noimageindex' => false, - 'is_robots_nosnippet' => false, - 'primary_focus_keyword' => 'focuskeyword', - 'primary_focus_keyword_score' => 100, - 'readability_score' => 50, - 'number_of_pages' => null, - 'is_public' => 0, - 'post_status' => 'publish', - 'is_protected' => false, - 'author_id' => 1, - 'post_parent' => 0, - 'has_public_posts' => false, - 'blog_id' => 1, - 'schema_page_type' => 'FAQPage', - 'schema_article_type' => 'NewsArticle', - 'estimated_reading_time_minutes' => 11, - 'version' => 2, - 'object_published_at' => '1234-12-12 00:00:00', - 'object_last_modified' => '1234-12-12 00:00:00', + 'object_id' => 1, + 'object_type' => 'post', + 'object_sub_type' => 'post', + 'permalink' => 'https://permalink', + 'canonical' => 'https://canonical', + 'title' => 'title', + 'breadcrumb_title' => 'breadcrumb_title', + 'description' => 'description', + 'open_graph_title' => 'open_graph_title', + 'open_graph_image' => 'open_graph_image', + 'open_graph_image_id' => 'open_graph_image_id', + 'open_graph_description' => 'open_graph_description', + 'twitter_title' => 'twitter_title', + 'twitter_image' => 'twitter_image', + 'twitter_image_id' => null, + 'twitter_description' => 'twitter_description', + 'is_cornerstone' => true, + 'is_robots_noindex' => true, + 'is_robots_nofollow' => true, + 'is_robots_noarchive' => false, + 'is_robots_noimageindex' => false, + 'is_robots_nosnippet' => false, + 'primary_focus_keyword' => 'focuskeyword', + 'primary_focus_keyword_score' => 100, + 'readability_score' => 50, + 'number_of_pages' => null, + 'is_publicly_viewable' => true, + 'number_of_publicly_viewable_posts' => 0, + 'post_status' => 'publish', + 'is_protected' => false, + 'author_id' => 1, + 'post_parent' => 0, + 'blog_id' => 1, + 'schema_page_type' => 'FAQPage', + 'schema_article_type' => 'NewsArticle', + 'estimated_reading_time_minutes' => 11, + 'version' => 3, + 'object_published_at' => '1234-12-12 00:00:00', + 'object_last_modified' => '1234-12-12 00:00:00', ]; $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); $this->set_indexable_set_expectations( $this->indexable, $indexable_expectations ); + $this->indexable->expects( 'set_deprecated_property' )->with( 'is_public', 0 ); // Reset all social images first. $this->set_indexable_set_expectations( @@ -339,10 +349,16 @@ public function test_build() { $this->twitter_image_set_by_user(); // We expect the open graph image, its source and its metadata to be set. - $this->indexable->orm->expects( 'set' )->with( 'open_graph_image_source', 'set-by-user' ); - $this->indexable->orm->expects( 'set' ) + $this->indexable->orm + ->expects( 'set' ) + ->with( 'open_graph_image_source', 'set-by-user' ); + + $this->indexable->orm + ->expects( 'set' ) ->with( 'open_graph_image', 'http://basic.wordpress.test/wp-content/uploads/2020/07/WordPress5.jpg' ); - $this->indexable->orm->expects( 'set' ) + + $this->indexable->orm + ->expects( 'set' ) // phpcs:ignore Yoast.Yoast.AlternativeFunctions.json_encode_json_encodeWithAdditionalParams -- Test code, mocking WP. ->with( 'open_graph_image_meta', \json_encode( $image_meta, ( \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES ) ) ); @@ -354,9 +370,6 @@ public function test_build() { $this->indexable->orm->expects( 'get' )->with( 'is_protected' )->andReturnFalse(); $this->indexable->orm->expects( 'get' )->with( 'is_robots_noindex' )->andReturn( true ); - // Has public posts. - $this->indexable->orm->expects( 'get' )->with( 'object_sub_type' )->andReturn( 'post' ); - // Breadcrumb title. $this->indexable->orm->expects( 'set' )->with( 'breadcrumb_title', null ); $this->indexable->orm->expects( 'offsetExists' )->with( 'breadcrumb_title' )->andReturnFalse(); @@ -364,6 +377,17 @@ public function test_build() { Monkey\Functions\expect( 'get_the_title' )->with( 1 )->andReturn( 'breadcrumb_title' ); Monkey\Functions\expect( 'wp_strip_all_tags' )->with( 'breadcrumb_title', true )->andReturn( 'breadcrumb_title' ); + Monkey\Functions\expect( 'get_post_type' )->andReturn( 'post-type' ); + Monkey\Functions\expect( 'get_post_status' )->andReturn( 'published' ); + Monkey\Functions\expect( 'is_post_type_viewable' )->andReturn( true ); + Monkey\Functions\expect( 'get_post_status_object' )->andReturn( + (object) [ + 'publicly_queryable' => true, + 'internal' => false, + 'protected' => false, + ] + ); + // Blog ID. Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); @@ -378,7 +402,7 @@ public function test_build() { public function test_build_post_not_indexable() { $this->indexable = Mockery::mock( Indexable::class ); - $this->post + $this->post_helper ->expects( 'is_post_indexable' ) ->with( 1 ) ->andReturn( false ); @@ -395,15 +419,18 @@ public function test_find_alternative_image_from_attachment() { $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_sub_type' ) ->andReturn( 'attachment' ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_id' ) ->andReturn( 123 ); - $this->image->allows( 'is_valid_attachment' ) + $this->image + ->allows( 'is_valid_attachment' ) ->with( 123 ) ->andReturn( true ); @@ -426,15 +453,18 @@ public function test_find_alternative_image_from_featured_image() { $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_sub_type' ) ->andReturn( 'post' ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_id' ) ->andReturn( 123 ); - $this->image->allows( 'get_featured_image_id' ) + $this->image + ->allows( 'get_featured_image_id' ) ->with( 123 ) ->andReturn( 456 ); @@ -458,15 +488,18 @@ public function test_find_alternative_image_from_gallery() { $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_sub_type' ) ->andReturn( 'post' ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_id' ) ->andReturn( 123 ); - $this->image->allows( 'get_featured_image_id' ) + $this->image + ->allows( 'get_featured_image_id' ) ->with( 123 ) ->andReturn( false ); @@ -482,7 +515,8 @@ public function test_find_alternative_image_from_gallery() { 'type' => 'image/jpeg', ]; - $this->image->allows( 'get_gallery_image' ) + $this->image + ->allows( 'get_gallery_image' ) ->with( 123 ) ->andReturn( $image_meta ); @@ -506,19 +540,23 @@ public function test_find_alternative_image_from_post_content() { $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_sub_type' ) ->andReturn( 'post' ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_id' ) ->andReturn( 123 ); - $this->image->allows( 'get_featured_image_id' ) + $this->image + ->allows( 'get_featured_image_id' ) ->with( 123 ) ->andReturn( false ); - $this->image->allows( 'get_gallery_image' ) + $this->image + ->allows( 'get_gallery_image' ) ->with( 123 ) ->andReturn( false ); @@ -534,7 +572,8 @@ public function test_find_alternative_image_from_post_content() { 'type' => 'image/jpeg', ]; - $this->image->allows( 'get_post_content_image' ) + $this->image + ->allows( 'get_post_content_image' ) ->with( 123 ) ->andReturn( $image_meta ); @@ -558,23 +597,28 @@ public function test_find_alternative_image_no_image() { $this->indexable = Mockery::mock( Indexable::class ); $this->indexable->orm = Mockery::mock( ORM::class ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_sub_type' ) ->andReturn( 'post' ); - $this->indexable->orm->allows( 'get' ) + $this->indexable->orm + ->allows( 'get' ) ->with( 'object_id' ) ->andReturn( 123 ); - $this->image->allows( 'get_featured_image_id' ) + $this->image + ->allows( 'get_featured_image_id' ) ->with( 123 ) ->andReturn( false ); - $this->image->allows( 'get_gallery_image' ) + $this->image + ->allows( 'get_gallery_image' ) ->with( 123 ) ->andReturn( false ); - $this->image->allows( 'get_post_content_image' ) + $this->image + ->allows( 'get_post_content_image' ) ->with( 123 ) ->andReturn( false ); @@ -716,7 +760,7 @@ public function test_is_public_post_status_is_not_public() { $this->indexable->object_sub_type = 'post'; $this->indexable->post_status = 'private'; - $this->post->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); + $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); $this->assertFalse( $this->instance->is_public( $this->indexable ) ); } @@ -732,7 +776,7 @@ public function test_is_public_post_noindex_false() { $this->indexable->object_sub_type = 'post'; $this->indexable->post_status = 'publish'; - $this->post->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); + $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); $this->assertTrue( $this->instance->is_public( $this->indexable ) ); } @@ -748,7 +792,7 @@ public function test_is_public_post_noindex_null() { $this->indexable->object_sub_type = 'post'; $this->indexable->post_status = 'publish'; - $this->post->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); + $this->post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); $this->assertNull( $this->instance->is_public( $this->indexable ) ); } @@ -786,96 +830,18 @@ public function test_is_public_attachment_with_post_parent() { $this->assertNull( $this->instance->is_public_attachment( $this->indexable ) ); } - /** - * Tests has_public_posts for when the indexable does not represent an attachment. - * - * @covers ::has_public_posts - */ - public function test_has_public_posts_no_attachment() { - $this->indexable->object_sub_type = 'post'; - - $this->assertNull( $this->instance->has_public_posts( $this->indexable ) ); - } - - /** - * Tests has_public_posts for when the attachment does not have a post parent. - * - * @covers ::has_public_posts - */ - public function test_has_public_posts_attachment_no_parent() { - $this->indexable->object_sub_type = 'attachment'; - $this->indexable->post_parent = 0; - - $this->assertFalse( $this->instance->has_public_posts( $this->indexable ) ); - } - - /** - * Tests has_public_posts for when the attachment does not have the post status inherit. - * - * @covers ::has_public_posts - */ - public function test_has_public_posts_attachment_no_inherit() { - $this->indexable->object_sub_type = 'attachment'; - $this->indexable->post_parent = 1; - $this->indexable->post_status = 'private'; - - $this->assertFalse( $this->instance->has_public_posts( $this->indexable ) ); - } - - /** - * Tests has_public_posts for when the attachment has a post parent. - * - * @covers ::has_public_posts - */ - public function test_has_public_posts_attachment_with_post_parent() { - $this->indexable->object_sub_type = 'attachment'; - $this->indexable->post_parent = 1; - $this->indexable->post_status = 'inherit'; - - $post_parent_indexable = Mockery::mock(); - $post_parent_indexable->is_public = true; - - $this->indexable_repository->expects( 'find_by_id_and_type' ) - ->once() - ->with( 1, 'post' ) - ->andReturn( $post_parent_indexable ); - - $this->assertTrue( $this->instance->has_public_posts( $this->indexable ) ); - } - - /** - * Tests has_public_posts for when the attachment has a post parent but the ORM throws an false. - * - * @covers ::has_public_posts - */ - public function test_has_public_posts_attachment_with_post_parent_false() { - $this->indexable->object_sub_type = 'attachment'; - $this->indexable->post_parent = 1; - $this->indexable->post_status = 'inherit'; - - $post_parent_indexable = Mockery::mock(); - $post_parent_indexable->is_public = true; - - $this->indexable_repository->expects( 'find_by_id_and_type' ) - ->once() - ->with( 1, 'post' ) - ->andReturn( false ); - - $this->assertFalse( $this->instance->has_public_posts( $this->indexable ) ); - } - /** * Tests that build throws an exception when no post could be found. * * @covers ::build */ public function test_build_term_null() { - $this->post + $this->post_helper ->expects( 'is_post_indexable' ) ->with( 1 ) ->andReturn( true ); - $this->post->expects( 'get_post' )->once()->with( 1 )->andReturn( null ); + $this->post_helper->expects( 'get_post' )->once()->with( 1 )->andReturn( null ); $this->expectException( Post_Not_Found_Exception::class ); @@ -892,12 +858,13 @@ public function test_build_term_null() { public function test_build_post_type_excluded() { $post_id = 1; - $this->post + $this->post_helper ->expects( 'is_post_indexable' ) ->with( $post_id ) ->andReturn( true ); - $this->post->expects( 'get_post' ) + $this->post_helper + ->expects( 'get_post' ) ->once() ->with( $post_id ) ->andReturn( @@ -906,7 +873,8 @@ public function test_build_post_type_excluded() { ] ); - $this->post_type_helper->expects( 'is_excluded' ) + $this->post_type_helper + ->expects( 'is_excluded' ) ->once() ->andReturnTrue(); diff --git a/tests/unit/builders/indexable-post-type-archive-builder-test.php b/tests/unit/builders/indexable-post-type-archive-builder-test.php index 9b85d64b121..6cee5340d22 100644 --- a/tests/unit/builders/indexable-post-type-archive-builder-test.php +++ b/tests/unit/builders/indexable-post-type-archive-builder-test.php @@ -8,6 +8,7 @@ use Yoast\WP\SEO\Builders\Indexable_Post_Type_Archive_Builder; 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\Tests\Unit\TestCase; use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions; @@ -15,13 +16,13 @@ /** * Class Indexable_Author_Test. * - * @group indexables - * @group builders + * @group indexables + * @group builders * * @coversDefaultClass \Yoast\WP\SEO\Builders\Indexable_Author_Builder * @covers \Yoast\WP\SEO\Builders\Indexable_Post_Builder * - * @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded + * @phpcs :disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded */ class Indexable_Post_Type_Archive_Builder_Test extends TestCase { @@ -47,22 +48,32 @@ public function test_build() { $post_helper = Mockery::mock( Post_Helper::class ); $post_helper->expects( 'get_public_post_statuses' )->once()->andReturn( [ 'publish' ] ); + $post_type_helper = Mockery::mock( Post_Type_Helper::class ); + $post_type_helper + ->expects( 'has_publicly_viewable_archive' ) + ->once() + ->with( 'my-post-type' ) + ->andReturn( true ); + $wpdb = Mockery::mock( 'wpdb' ); $wpdb->posts = 'wp_posts'; $wpdb->expects( 'prepare' )->once()->with( " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$wpdb->posts} AS p WHERE p.post_status IN (%s) - AND p.post_password = '' AND p.post_type = %s ", [ 'publish', 'my-post-type' ] )->andReturn( 'PREPARED_QUERY' ); $wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', + 'number_of_public_posts' => 6, + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', ] ); @@ -75,16 +86,21 @@ public function test_build() { $indexable_mock->orm->expects( 'set' )->with( 'permalink', 'https://permalink' ); $indexable_mock->orm->expects( 'set' )->with( 'description', 'my_post_type_meta_description' ); $indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', false ); - $indexable_mock->orm->expects( 'set' )->with( 'is_public', true ); + $indexable_mock->orm->expects( 'set' )->with( 'is_publicly_viewable', true ); + $indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 6 ); + $indexable_mock->expects( 'set_deprecated_property' )->with( 'is_public', true ); + $indexable_mock->orm->expects( 'get' )->with( 'is_robots_noindex' )->andReturnFalse(); + $indexable_mock->orm->expects( 'get' )->with( 'object_sub_type' )->andReturn( 'my-post-type' ); Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); $indexable_mock->orm->expects( 'set' )->with( 'version', 1 ); + $indexable_mock->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-01-01 00:00:00' ); $indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); - $builder = new Indexable_Post_Type_Archive_Builder( $options_mock, $versions, $post_helper, $wpdb ); + $builder = new Indexable_Post_Type_Archive_Builder( $options_mock, $versions, $post_helper, $post_type_helper, $wpdb ); $builder->build( 'my-post-type', $indexable_mock ); } } diff --git a/tests/unit/builders/indexable-system-page-builder-test.php b/tests/unit/builders/indexable-system-page-builder-test.php index c1c1f717582..22e4b6e720b 100644 --- a/tests/unit/builders/indexable-system-page-builder-test.php +++ b/tests/unit/builders/indexable-system-page-builder-test.php @@ -39,10 +39,12 @@ public function test_build() { $indexable_mock->orm->expects( 'set' )->with( 'object_sub_type', 'search-result' ); $indexable_mock->orm->expects( 'set' )->with( 'title', 'search_title' ); $indexable_mock->orm->expects( 'set' )->with( 'is_robots_noindex', true ); + $indexable_mock->orm->expects( 'set' )->with( 'is_publicly_viewable', true ); + $indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 0 ); Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); - $indexable_mock->orm->expects( 'set' )->with( 'version', 1 ); + $indexable_mock->orm->expects( 'set' )->with( 'version', 2 ); $builder = new Indexable_System_Page_Builder( $options_mock, new Indexable_Builder_Versions() ); $builder->build( 'search-result', $indexable_mock ); diff --git a/tests/unit/builders/indexable-term-builder-test.php b/tests/unit/builders/indexable-term-builder-test.php index 02dc49728b0..0bd0b71d2e3 100644 --- a/tests/unit/builders/indexable-term-builder-test.php +++ b/tests/unit/builders/indexable-term-builder-test.php @@ -4,6 +4,7 @@ use Brain\Monkey; use Mockery; +use wpdb; use Yoast\WP\Lib\ORM; use Yoast\WP\SEO\Builders\Indexable_Term_Builder; use Yoast\WP\SEO\Exceptions\Indexable\Invalid_Term_Exception; @@ -21,8 +22,8 @@ /** * Class Indexable_Term_Builder_Test. * - * @group indexables - * @group builders + * @group indexables + * @group builders * * @coversDefaultClass \Yoast\WP\SEO\Builders\Indexable_Term_Builder * @covers \Yoast\WP\SEO\Builders\Indexable_Term_Builder @@ -150,19 +151,23 @@ protected function set_indexable_set_expectations( $indexable_mock, $expectation * @param Mockery\Mock|Indexable $indexable_mock The mocked indexable. */ protected function twitter_image_set_by_user( $indexable_mock ) { - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image' ) ->andReturn( 'twitter-image' ); - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image_id' ) ->andReturn( 'twitter-image-id' ); - $this->twitter_image->shouldReceive( 'get_by_id' ) + $this->twitter_image + ->shouldReceive( 'get_by_id' ) ->with( 'twitter-image-id' ) ->andReturn( 'twitter_image' ); - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->with( 'twitter_image_source' ) ->andReturn( 'set-by-user' ); } @@ -174,20 +179,24 @@ protected function twitter_image_set_by_user( $indexable_mock ) { * @param array $image_meta The mocked meta data of the image. */ protected function open_graph_image_set_by_user( $indexable_mock, $image_meta ) { - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->with( 'open_graph_image' ) ->andReturn( 'open-graph-image' ); - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->twice() ->with( 'open_graph_image_id' ) ->andReturn( 'open-graph-image-id' ); - $indexable_mock->orm->shouldReceive( 'get' ) + $indexable_mock->orm + ->shouldReceive( 'get' ) ->with( 'open_graph_image_source' ) ->andReturn( 'set-by-user' ); - $this->open_graph_image->shouldReceive( 'get_image_by_id' ) + $this->open_graph_image + ->shouldReceive( 'get_image_by_id' ) ->with( 'open-graph-image-id' ) ->andReturn( $image_meta ); } @@ -220,8 +229,10 @@ public function test_build() { Monkey\Functions\expect( 'get_term' )->once()->with( 1 )->andReturn( $term ); Monkey\Functions\expect( 'get_term_link' )->once()->with( $term, 'category' )->andReturn( 'https://example.org/category/1' ); Monkey\Functions\expect( 'is_wp_error' )->twice()->andReturn( false ); + Monkey\Functions\expect( 'is_taxonomy_viewable' )->once()->with( 'category' )->andReturn( true ); - $this->taxonomy->expects( 'get_term_meta' ) + $this->taxonomy + ->expects( 'get_term_meta' ) ->once() ->with( $term ) ->andReturn( @@ -250,7 +261,10 @@ public function test_build() { $this->wpdb->expects( 'prepare' )->once()->with( " - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at + SELECT + COUNT(p.ID) as number_of_public_posts, + MAX(p.post_modified_gmt) AS most_recent_last_modified, + MIN(p.post_date_gmt) AS first_published_at FROM {$this->wpdb->posts} AS p INNER JOIN {$this->wpdb->term_relationships} AS term_rel ON term_rel.object_id = p.ID @@ -259,14 +273,14 @@ public function test_build() { AND term_tax.taxonomy = %s AND term_tax.term_id = %d WHERE p.post_status IN (%s) - AND p.post_password = '' ", [ 'category', 1, 'publish' ] )->andReturn( 'PREPARED_QUERY' ); $this->wpdb->expects( 'get_row' )->once()->with( 'PREPARED_QUERY' )->andReturn( (object) [ - 'last_modified' => '1234-12-12 00:00:00', - 'published_at' => '1234-12-12 00:00:00', + 'number_of_public_posts' => 10, + 'most_recent_last_modified' => '1234-12-12 23:59:59', + 'first_published_at' => '1234-12-12 00:00:00', ] ); @@ -296,6 +310,7 @@ public function test_build() { 'is_robots_noarchive' => null, 'is_robots_noimageindex' => null, 'is_robots_nosnippet' => null, + 'is_publicly_viewable' => true, 'primary_focus_keyword' => 'focuskeyword', 'primary_focus_keyword_score' => 75, 'readability_score' => 50, @@ -303,6 +318,7 @@ public function test_build() { ]; $this->set_indexable_set_expectations( $indexable_mock, $indexable_expectations ); + $indexable_mock->expects( 'set_deprecated_property' )->with( 'is_public', false ); // Reset all social images first. $this->set_indexable_set_expectations( @@ -335,10 +351,14 @@ public function test_build() { $this->twitter_image_set_by_user( $indexable_mock ); // We expect the open graph image, its source and its metadata to be set. - $indexable_mock->orm->expects( 'set' )->with( 'open_graph_image_source', 'set-by-user' ); - $indexable_mock->orm->expects( 'set' ) + $indexable_mock->orm + ->expects( 'set' ) + ->with( 'open_graph_image_source', 'set-by-user' ); + $indexable_mock->orm + ->expects( 'set' ) ->with( 'open_graph_image', 'http://basic.wordpress.test/wp-content/uploads/2020/07/WordPress5.jpg' ); - $indexable_mock->orm->expects( 'set' ) + $indexable_mock->orm + ->expects( 'set' ) // phpcs:ignore Yoast.Yoast.AlternativeFunctions.json_encode_json_encodeWithAdditionalParams -- Test code, mocking WP. ->with( 'open_graph_image_meta', \json_encode( $image_meta, ( \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES ) ) ); @@ -350,12 +370,16 @@ public function test_build() { $indexable_mock->orm->expects( 'set' )->once()->with( 'breadcrumb_title', null ); $indexable_mock->orm->expects( 'get' )->twice()->with( 'is_robots_noindex' )->andReturn( true ); - $indexable_mock->orm->expects( 'set' )->once()->with( 'is_public', false ); + + $indexable_mock->orm->expects( 'get' )->with( 'object_id' )->andReturn( 1 ); + $indexable_mock->orm->expects( 'get' )->with( 'object_sub_type' )->andReturn( 'category' ); Monkey\Functions\expect( 'get_current_blog_id' )->once()->andReturn( 1 ); $indexable_mock->orm->expects( 'set' )->with( 'blog_id', 1 ); + $indexable_mock->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-01-01 00:00:00' ); $indexable_mock->orm->expects( 'set' )->with( 'object_published_at', '1234-12-12 00:00:00' ); - $indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 00:00:00' ); + $indexable_mock->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 23:59:59' ); + $indexable_mock->orm->expects( 'set' )->with( 'number_of_publicly_viewable_posts', 10 ); $this->instance->build( 1, $indexable_mock ); } @@ -462,13 +486,15 @@ public function test_find_alternative_image_content_image() { $indexable_mock->orm = Mockery::mock( ORM::class ); $object_id = 123; - $indexable_mock->orm->expects( 'get' ) + $indexable_mock->orm + ->expects( 'get' ) ->with( 'object_id' ) ->andReturn( $object_id ); $image = 'http://basic.wordpress.test/wp-content/uploads/2020/07/WordPress5.jpg'; - $this->image->expects( 'get_term_content_image' ) + $this->image + ->expects( 'get_term_content_image' ) ->with( $object_id ) ->andReturn( $image ); @@ -487,11 +513,13 @@ public function test_find_alternative_image_no_content_image() { $indexable_mock->orm = Mockery::mock( ORM::class ); $object_id = 123; - $indexable_mock->orm->expects( 'get' ) + $indexable_mock->orm + ->expects( 'get' ) ->with( 'object_id' ) ->andReturn( $object_id ); - $this->image->expects( 'get_term_content_image' ) + $this->image + ->expects( 'get_term_content_image' ) ->with( $object_id ) ->andReturn( null ); diff --git a/tests/unit/doubles/builders/indexable-post-builder-double.php b/tests/unit/doubles/builders/indexable-post-builder-double.php index 752b6161d75..054304b654f 100644 --- a/tests/unit/doubles/builders/indexable-post-builder-double.php +++ b/tests/unit/doubles/builders/indexable-post-builder-double.php @@ -33,17 +33,6 @@ public function is_public_attachment( $indexable ) { return parent::is_public_attachment( $indexable ); } - /** - * 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. - */ - public function has_public_posts( $indexable ) { - return parent::has_public_posts( $indexable ); - } - /** * Gets the number of pages for a post. * diff --git a/tests/unit/doubles/integrations/watchers/indexable-post-watcher-double.php b/tests/unit/doubles/integrations/watchers/indexable-post-watcher-double.php index 8b957d989bf..b138fba7e06 100644 --- a/tests/unit/doubles/integrations/watchers/indexable-post-watcher-double.php +++ b/tests/unit/doubles/integrations/watchers/indexable-post-watcher-double.php @@ -11,15 +11,6 @@ */ class Indexable_Post_Watcher_Double extends Indexable_Post_Watcher { - /** - * Updates the has_public_posts when the post indexable is built. - * - * @param Indexable $indexable The indexable to check. - */ - public function update_has_public_posts( $indexable ) { - parent::update_has_public_posts( $indexable ); - } - /** * Updates the relations on post save or post status change. * diff --git a/tests/unit/helpers/author-archive-helper-test.php b/tests/unit/helpers/author-archive-helper-test.php index f11545b7b51..5fd053bbf5d 100644 --- a/tests/unit/helpers/author-archive-helper-test.php +++ b/tests/unit/helpers/author-archive-helper-test.php @@ -49,39 +49,4 @@ public function test_get_author_archive_post_types_apply_filter() { $this->assertEquals( $expected, $this->instance->get_author_archive_post_types() ); } - - /** - * Tests that true is returned when the author has a public post. - * - * @covers ::author_has_public_posts - */ - public function test_author_has_public_posts_with_public_post() { - $this->instance->expects( 'author_has_a_public_post' )->once()->with( 1 )->andReturnTrue(); - - $this->assertTrue( $this->instance->author_has_public_posts( 1 ) ); - } - - /** - * Tests that null is returned when the author has a post without noindex override. - * - * @covers ::author_has_public_posts - */ - public function test_author_has_public_posts_with_post_without_override() { - $this->instance->expects( 'author_has_a_public_post' )->once()->with( 1 )->andReturnFalse(); - $this->instance->expects( 'author_has_a_post_with_is_public_null' )->once()->with( 1 )->andReturnTrue(); - - $this->assertNull( $this->instance->author_has_public_posts( 1 ) ); - } - - /** - * Tests that false is returned when the author has no public posts and no posts without an override. - * - * @covers ::author_has_public_posts - */ - public function test_author_has_public_posts_without_public_or_override_posts() { - $this->instance->expects( 'author_has_a_public_post' )->once()->with( 1 )->andReturnFalse(); - $this->instance->expects( 'author_has_a_post_with_is_public_null' )->once()->with( 1 )->andReturnFalse(); - - $this->assertFalse( $this->instance->author_has_public_posts( 1 ) ); - } } diff --git a/tests/unit/helpers/robots-helper-test.php b/tests/unit/helpers/robots-helper-test.php index 57d71f72f3f..d96c3aa7fc0 100644 --- a/tests/unit/helpers/robots-helper-test.php +++ b/tests/unit/helpers/robots-helper-test.php @@ -3,6 +3,8 @@ namespace Yoast\WP\SEO\Tests\Unit\Helpers; use Brain\Monkey; +use Mockery\MockInterface; +use Yoast\WP\SEO\Helpers\Options_Helper; use Yoast\WP\SEO\Helpers\Robots_Helper; use Yoast\WP\SEO\Tests\Unit\TestCase; @@ -22,13 +24,20 @@ class Robots_Helper_Test extends TestCase { */ private $instance; + /** + * A helper class that manages options. + * + * @var Options_Helper|MockInterface + */ + protected $options_helper; + /** * Sets up the test class. */ protected function set_up() { parent::set_up(); - - $this->instance = new Robots_Helper(); + $this->options_helper = \Mockery::mock( Options_Helper::class ); + $this->instance = new Robots_Helper( $this->options_helper ); } /** diff --git a/tests/unit/integrations/watchers/indexable-post-watcher-test.php b/tests/unit/integrations/watchers/indexable-post-watcher-test.php index c865dc223c1..6014fa2c950 100644 --- a/tests/unit/integrations/watchers/indexable-post-watcher-test.php +++ b/tests/unit/integrations/watchers/indexable-post-watcher-test.php @@ -2,10 +2,8 @@ namespace Yoast\WP\SEO\Tests\Unit\Integrations\Watchers; -use Brain\Monkey; use Exception; use Mockery; -use WP_Post; use Yoast\WP\Lib\ORM; use Yoast\WP\SEO\Builders\Indexable_Builder; use Yoast\WP\SEO\Builders\Indexable_Link_Builder; @@ -19,6 +17,7 @@ use Yoast\WP\SEO\Tests\Unit\Doubles\Integrations\Watchers\Indexable_Post_Watcher_Double; use Yoast\WP\SEO\Tests\Unit\Doubles\Models\Indexable_Mock; use Yoast\WP\SEO\Tests\Unit\TestCase; +use function Brain\Monkey\Functions\expect; /** * Class Indexable_Post_Watcher_Test. @@ -55,7 +54,7 @@ class Indexable_Post_Watcher_Test extends TestCase { /** * The link builder. * - * @var Indexable_Link_Builder + * @var Mockery\MockInterface|Indexable_Link_Builder */ protected $link_builder; @@ -69,7 +68,7 @@ class Indexable_Post_Watcher_Test extends TestCase { /** * Represents the class we are testing. * - * @var Indexable_Post_Watcher_Double + * @var Mockery\MockInterface|Indexable_Post_Watcher_Double */ private $instance; @@ -164,19 +163,12 @@ public function test_delete_indexable() { $this->hierarchy_repository->expects( 'clear_ancestors' )->once()->with( $id )->andReturn( true ); $this->link_builder->expects( 'delete' )->once()->with( $indexable ); - $this->post->expects( 'get_post' )->once()->with( $id )->andReturn( $post ); - $this->instance ->expects( 'update_relations' ) ->with( $post ) ->once(); - $this->instance - ->expects( 'update_has_public_posts' ) - ->with( $indexable ) - ->once(); - - $this->instance->delete_indexable( $id ); + $this->instance->delete_indexable( $id, $post ); } /** @@ -185,11 +177,18 @@ public function test_delete_indexable() { * @covers ::delete_indexable */ public function test_delete_indexable_does_not_exist() { - $id = 1; + $id = 1; + $post = (object) []; $this->repository->expects( 'find_by_id_and_type' )->once()->with( $id, 'post', false )->andReturn( false ); - $this->instance->delete_indexable( $id ); + + $this->repository->expects( 'find_by_id_and_type' )->never(); + $this->hierarchy_repository->expects( 'clear_ancestors' )->never(); + $this->link_builder->expects( 'delete' )->never(); + $this->instance->expects( 'update_relations' )->never(); + + $this->instance->delete_indexable( $id, $post ); } /** @@ -346,67 +345,6 @@ public function test_updated_indexable_non_post() { $this->instance->updated_indexable( $indexable, $post ); } - /** - * Tests that update_has_public_posts updates the author archive too. - * - * @covers ::update_has_public_posts - */ - public function test_update_has_public_posts_with_post() { - $post_indexable = Mockery::mock(); - $post_indexable->object_id = 33; - $post_indexable->object_sub_type = 'post'; - $post_indexable->author_id = 1; - $post_indexable->is_public = null; - - $author_indexable = Mockery::mock( Indexable_Mock::class ); - $author_indexable->object_id = 11; - - $this->repository - ->expects( 'find_by_id_and_type' ) - ->with( 1, 'user' ) - ->once() - ->andReturn( $author_indexable ); - - $this->author_archive - ->expects( 'author_has_public_posts' ) - ->with( 11 ) - ->once() - ->andReturn( true ); - $author_indexable->expects( 'save' )->once(); - - $this->post->expects( 'update_has_public_posts_on_attachments' )->once()->with( 33, null )->andReturnTrue(); - - $this->instance->update_has_public_posts( $post_indexable ); - - $this->assertTrue( $author_indexable->has_public_posts ); - } - - /** - * Tests that update_has_public_posts updates the author archive . - * - * @covers ::update_has_public_posts - */ - public function test_update_has_public_posts_with_post_throwing_exceptions() { - $post_indexable = Mockery::mock(); - $post_indexable->object_id = 33; - $post_indexable->object_sub_type = 'post'; - $post_indexable->author_id = 1; - $post_indexable->is_public = null; - - $this->repository->expects( 'find_by_id_and_type' ) - ->with( 1, 'user' ) - ->once() - ->andThrow( new Exception( 'an error' ) ); - $this->author_archive->expects( 'author_has_public_posts' )->never(); - $this->post->expects( 'update_has_public_posts_on_attachments' ) - ->once() - ->with( 33, null ) - ->andReturnTrue(); - $this->logger->expects( 'log' )->once()->with( 'error', 'an error' ); - - $this->instance->update_has_public_posts( $post_indexable ); - } - /** * Tests the routine for updating the relations. * @@ -420,11 +358,12 @@ public function test_update_relations() { 'post_modified_gmt' => '1234-12-12 12:12:12', ]; + expect( 'current_time' )->with( 'mysql' )->andReturn( '1234-12-12 12:12:12' ); + $indexable = Mockery::mock( Indexable_Mock::class ); $indexable->orm = Mockery::mock( ORM::class ); - $indexable->orm->expects( 'get' )->with( 'object_last_modified' )->andReturn( '1234-12-12 00:00:00' ); + $indexable->orm->expects( 'set' )->with( 'object_last_modified', '1234-12-12 12:12:12' ); - $indexable->expects( 'save' )->once(); $this->instance ->expects( 'get_related_indexables' ) @@ -432,6 +371,11 @@ public function test_update_relations() { ->with( $post ) ->andReturn( [ $indexable ] ); + $this->builder + ->expects( 'recalculate_aggregates' ) + ->once() + ->with( $indexable ); + $this->instance->update_relations( $post ); } @@ -447,12 +391,18 @@ public function test_update_relations_with_no_indexables_found() { 'ID' => 1, ]; + expect( 'current_time' )->with( 'mysql' )->andReturn( '1234-12-12 12:12:12' ); + $this->instance ->expects( 'get_related_indexables' ) ->once() ->with( $post ) ->andReturn( [] ); + $this->builder + ->expects( 'recalculate_aggregates' ) + ->never(); + $this->instance->update_relations( $post ); } @@ -468,7 +418,7 @@ public function test_get_related_indexables() { 'ID' => 1, ]; - Monkey\Functions\expect( 'get_post_taxonomies' ) + expect( 'get_post_taxonomies' ) ->once() ->with( 1 ) ->andReturn( @@ -478,17 +428,17 @@ public function test_get_related_indexables() { ] ); - Monkey\Functions\expect( 'is_taxonomy_viewable' ) + expect( 'is_taxonomy_viewable' ) ->once() ->with( 'taxonomy' ) ->andReturn( true ); - Monkey\Functions\expect( 'is_taxonomy_viewable' ) + expect( 'is_taxonomy_viewable' ) ->once() ->with( 'another-taxonomy' ) ->andReturn( true ); - Monkey\Functions\expect( 'get_the_terms' ) + expect( 'get_the_terms' ) ->once() ->with( 1, 'taxonomy' ) ->andReturn( @@ -502,12 +452,12 @@ public function test_get_related_indexables() { ] ); - Monkey\Functions\expect( 'get_the_terms' ) + expect( 'get_the_terms' ) ->once() ->with( 1, 'another-taxonomy' ) ->andReturnNull(); - Monkey\Functions\expect( 'wp_list_pluck' ) + expect( 'wp_list_pluck' ) ->once() ->with( [ diff --git a/tests/unit/repositories/indexable-repository-test.php b/tests/unit/repositories/indexable-repository-test.php index a37e8d7a55e..a1334226c2b 100644 --- a/tests/unit/repositories/indexable-repository-test.php +++ b/tests/unit/repositories/indexable-repository-test.php @@ -7,6 +7,7 @@ use Yoast\WP\Lib\ORM; use Yoast\WP\SEO\Builders\Indexable_Builder; use Yoast\WP\SEO\Helpers\Current_Page_Helper; +use Yoast\WP\SEO\Helpers\Robots_Helper; use Yoast\WP\SEO\Loggers\Logger; use Yoast\WP\SEO\Models\Indexable; use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository; @@ -74,6 +75,13 @@ class Indexable_Repository_Test extends TestCase { */ protected $version_manager; + /** + * A helper class for robots meta tags. + * + * @var Mockery\MockInterface|Robots_Helper + */ + protected $robots_helper; + /** * Setup the test. */ @@ -86,6 +94,7 @@ protected function set_up() { $this->hierarchy_repository = Mockery::mock( Indexable_Hierarchy_Repository::class ); $this->wpdb = Mockery::mock( wpdb::class ); $this->version_manager = Mockery::mock( Indexable_Version_Manager::class ); + $this->robots_helper = Mockery::mock( Robots_Helper::class ); $this->instance = Mockery::mock( Indexable_Repository::class, [ @@ -95,6 +104,7 @@ protected function set_up() { $this->hierarchy_repository, $this->wpdb, $this->version_manager, + $this->robots_helper, ] )->makePartial(); }