| @@ -1,10 +1,11 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace Yoast\WP\SEO\Builders; |
| 4 | 4 | |
| 5 | -use wpdb; | |
| 5 | +use Yoast\WP\SEO\Exceptions\Indexable\Author_Not_Built_Exception; | |
| 6 | 6 | use Yoast\WP\SEO\Helpers\Author_Archive_Helper; |
| 7 | +use Yoast\WP\SEO\Helpers\Options_Helper; | |
| 7 | 8 | use Yoast\WP\SEO\Helpers\Post_Helper; |
| 8 | 9 | use Yoast\WP\SEO\Models\Indexable; |
| 9 | 10 | use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions; |
| 10 | 11 | |
| @@ -31,20 +32,20 @@ | ||
| 31 | 32 | */ |
| 32 | 33 | protected $version; |
| 33 | 34 | |
| 34 | 35 | /** |
| 35 | - * Holds the taxonomy helper instance. | |
| 36 | + * Holds the options helper instance. | |
| 36 | 37 | * |
| 37 | - * @var Post_Helper | |
| 38 | + * @var Options_Helper | |
| 38 | 39 | */ |
| 39 | - protected $post_helper; | |
| 40 | + protected $options_helper; | |
| 40 | 41 | |
| 41 | 42 | /** |
| 42 | - * The WPDB instance. | |
| 43 | + * Holds the taxonomy helper instance. | |
| 43 | 44 | * |
| 44 | - * @var wpdb | |
| 45 | + * @var Post_Helper | |
| 45 | 46 | */ |
| 46 | - protected $wpdb; | |
| 47 | + protected $post_helper; | |
| 47 | 48 | |
| 48 | 49 | /** |
| 49 | 50 | * Indexable_Author_Builder constructor. |
| 50 | 51 | * |
| @@ -49,21 +50,21 @@ | ||
| 49 | 50 | * Indexable_Author_Builder constructor. |
| 50 | 51 | * |
| 51 | 52 | * @param Author_Archive_Helper $author_archive The author archive helper. |
| 52 | 53 | * @param Indexable_Builder_Versions $versions The Indexable version manager. |
| 54 | + * @param Options_Helper $options_helper The options helper. | |
| 53 | 55 | * @param Post_Helper $post_helper The post helper. |
| 54 | - * @param wpdb $wpdb The WPDB instance. | |
| 55 | 56 | */ |
| 56 | 57 | public function __construct( |
| 57 | 58 | Author_Archive_Helper $author_archive, |
| 58 | 59 | Indexable_Builder_Versions $versions, |
| 59 | - Post_Helper $post_helper, | |
| 60 | - wpdb $wpdb | |
| 60 | + Options_Helper $options_helper, | |
| 61 | + Post_Helper $post_helper | |
| 61 | 62 | ) { |
| 62 | 63 | $this->author_archive = $author_archive; |
| 63 | 64 | $this->version = $versions->get_latest_version_for_type( 'user' ); |
| 65 | + $this->options_helper = $options_helper; | |
| 64 | 66 | $this->post_helper = $post_helper; |
| 65 | - $this->wpdb = $wpdb; | |
| 66 | 67 | } |
| 67 | 68 | |
| 68 | 69 | /** |
| 69 | 70 | * Formats the data. |
| @@ -71,10 +72,17 @@ | ||
| 71 | 72 | * @param int $user_id The user to retrieve the indexable for. |
| 72 | 73 | * @param Indexable $indexable The indexable to format. |
| 73 | 74 | * |
| 74 | 75 | * @return Indexable The extended indexable. |
| 76 | + * | |
| 77 | + * @throws Author_Not_Built_Exception When author is not built. | |
| 75 | 78 | */ |
| 76 | 79 | public function build( $user_id, Indexable $indexable ) { |
| 80 | + $exception = $this->check_if_user_should_be_indexed( $user_id ); | |
| 81 | + if ( $exception ) { | |
| 82 | + throw $exception; | |
| 83 | + } | |
| 84 | + | |
| 77 | 85 | $meta_data = $this->get_meta_data( $user_id ); |
| 78 | 86 | |
| 79 | 87 | $indexable->object_id = $user_id; |
| 80 | 88 | $indexable->object_type = 'user'; |
| @@ -154,9 +162,9 @@ | ||
| 154 | 162 | $indexable->object_id, |
| 155 | 163 | [ |
| 156 | 164 | 'size' => 500, |
| 157 | 165 | 'scheme' => 'https', |
| 158 | - ] | |
| 166 | + ], | |
| 159 | 167 | ); |
| 160 | 168 | if ( $gravatar_image ) { |
| 161 | 169 | return [ |
| 162 | 170 | 'image' => $gravatar_image, |
| @@ -174,20 +182,66 @@ | ||
| 174 | 182 | * |
| 175 | 183 | * @return object An object with last_modified and published_at timestamps. |
| 176 | 184 | */ |
| 177 | 185 | protected function get_object_timestamps( $author_id ) { |
| 186 | + global $wpdb; | |
| 178 | 187 | $post_statuses = $this->post_helper->get_public_post_statuses(); |
| 179 | 188 | |
| 180 | - $sql = " | |
| 181 | - SELECT MAX(p.post_modified_gmt) AS last_modified, MIN(p.post_date_gmt) AS published_at | |
| 182 | - FROM {$this->wpdb->posts} AS p | |
| 183 | - WHERE p.post_status IN (" . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") | |
| 184 | - AND p.post_password = '' | |
| 185 | - AND p.post_author = %d | |
| 186 | - "; | |
| 189 | + $replacements = []; | |
| 190 | + $replacements[] = 'post_modified_gmt'; | |
| 191 | + $replacements[] = 'post_date_gmt'; | |
| 192 | + $replacements[] = $wpdb->posts; | |
| 193 | + $replacements[] = 'post_status'; | |
| 194 | + $replacements = \array_merge( $replacements, $post_statuses ); | |
| 195 | + $replacements[] = 'post_password'; | |
| 196 | + $replacements[] = 'post_author'; | |
| 197 | + $replacements[] = $author_id; | |
| 187 | 198 | |
| 188 | - $replacements = \array_merge( $post_statuses, [ $author_id ] ); | |
| 199 | + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized. | |
| 200 | + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. | |
| 201 | + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. | |
| 202 | + return $wpdb->get_row( | |
| 203 | + $wpdb->prepare( | |
| 204 | + ' | |
| 205 | + SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at | |
| 206 | + FROM %i AS p | |
| 207 | + WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ") | |
| 208 | + AND p.%i = '' | |
| 209 | + AND p.%i = %d | |
| 210 | + ", | |
| 211 | + $replacements, | |
| 212 | + ), | |
| 213 | + ); | |
| 214 | + //phpcs:enable | |
| 215 | + } | |
| 189 | 216 | |
| 190 | - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- We are using wpdb prepare. | |
| 191 | - return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $replacements ) ); | |
| 217 | + /** | |
| 218 | + * Checks if the user should be indexed. | |
| 219 | + * Returns an exception with an appropriate message if not. | |
| 220 | + * | |
| 221 | + * @param string $user_id The user id. | |
| 222 | + * | |
| 223 | + * @return Author_Not_Built_Exception|null The exception if it should not be indexed, or `null` if it should. | |
| 224 | + */ | |
| 225 | + protected function check_if_user_should_be_indexed( $user_id ) { | |
| 226 | + $exception = null; | |
| 227 | + | |
| 228 | + if ( $this->author_archive->are_disabled() ) { | |
| 229 | + $exception = Author_Not_Built_Exception::author_archives_are_disabled( $user_id ); | |
| 230 | + } | |
| 231 | + // We will check if the author has public posts the WP way, instead of the indexable way, to make sure we get proper results even if SEO optimization is not run. | |
| 232 | + // In case the user has no public posts, we check if the user should be indexed anyway. | |
| 233 | + elseif ( $this->options_helper->get( 'noindex-author-noposts-wpseo', false ) === true && $this->author_archive->author_has_public_posts_wp( $user_id ) === false ) { | |
| 234 | + $exception = Author_Not_Built_Exception::author_archives_are_not_indexed_for_users_without_posts( $user_id ); | |
| 235 | + } | |
| 236 | + | |
| 237 | + /** | |
| 238 | + * Filter: Include or exclude a user from being build and saved as an indexable. | |
| 239 | + * Return an `Author_Not_Built_Exception` when the indexable should not be build, with an appropriate message telling why it should not be built. | |
| 240 | + * Return `null` if the indexable should be build. | |
| 241 | + * | |
| 242 | + * @param Author_Not_Built_Exception|null $exception An exception if the indexable is not being built, `null` if the indexable should be built. | |
| 243 | + * @param string $user_id The ID of the user that should or should not be excluded. | |
| 244 | + */ | |
| 245 | + return \apply_filters( 'wpseo_should_build_and_save_user_indexable', $exception, $user_id ); | |
| 192 | 246 | } |
| 193 | 247 | } |