PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / integrations / watchers / indexable-post-watcher.php

indexable-post-watcher.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/integrations/watchers/indexable-post-watcher.php

339 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Integrations\Watchers;
4
5 use Exception;
6 use WP_Post;
7 use Yoast\WP\SEO\Builders\Indexable_Builder;
8 use Yoast\WP\SEO\Builders\Indexable_Link_Builder;
9 use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
10 use Yoast\WP\SEO\Helpers\Author_Archive_Helper;
11 use Yoast\WP\SEO\Helpers\Indexable_Helper;
12 use Yoast\WP\SEO\Helpers\Post_Helper;
13 use Yoast\WP\SEO\Integrations\Cleanup_Integration;
14 use Yoast\WP\SEO\Integrations\Integration_Interface;
15 use Yoast\WP\SEO\Loggers\Logger;
16 use Yoast\WP\SEO\Models\Indexable;
17 use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
18 use Yoast\WP\SEO\Repositories\Indexable_Repository;
19 use YoastSEO_Vendor\Psr\Log\LogLevel;
20
21 /**
22 * WordPress Post watcher.
23 *
24 * Fills the Indexable according to Post data.
25 */
26 class Indexable_Post_Watcher implements Integration_Interface {
27
28 /**
29 * The indexable repository.
30 *
31 * @var Indexable_Repository
32 */
33 protected $repository;
34
35 /**
36 * The indexable builder.
37 *
38 * @var Indexable_Builder
39 */
40 protected $builder;
41
42 /**
43 * The indexable hierarchy repository.
44 *
45 * @var Indexable_Hierarchy_Repository
46 */
47 private $hierarchy_repository;
48
49 /**
50 * The link builder.
51 *
52 * @var Indexable_Link_Builder
53 */
54 protected $link_builder;
55
56 /**
57 * The author archive helper.
58 *
59 * @var Author_Archive_Helper
60 */
61 private $author_archive;
62
63 /**
64 * The indexable helper.
65 *
66 * @var Indexable_Helper
67 */
68 private $indexable_helper;
69
70 /**
71 * Holds the Post_Helper instance.
72 *
73 * @var Post_Helper
74 */
75 private $post;
76
77 /**
78 * Holds the logger.
79 *
80 * @var Logger
81 */
82 protected $logger;
83
84 /**
85 * Returns the conditionals based on which this loadable should be active.
86 *
87 * @return array<string> The conditionals.
88 */
89 public static function get_conditionals() {
90 return [ Migrations_Conditional::class ];
91 }
92
93 /**
94 * Indexable_Post_Watcher constructor.
95 *
96 * @param Indexable_Repository $repository The repository to use.
97 * @param Indexable_Builder $builder The post builder to use.
98 * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository to use.
99 * @param Indexable_Link_Builder $link_builder The link builder.
100 * @param Author_Archive_Helper $author_archive The author archive helper.
101 * @param Indexable_Helper $indexable_helper The indexable helper.
102 * @param Post_Helper $post The post helper.
103 * @param Logger $logger The logger.
104 */
105 public function __construct(
106 Indexable_Repository $repository,
107 Indexable_Builder $builder,
108 Indexable_Hierarchy_Repository $hierarchy_repository,
109 Indexable_Link_Builder $link_builder,
110 Author_Archive_Helper $author_archive,
111 Indexable_Helper $indexable_helper,
112 Post_Helper $post,
113 Logger $logger
114 ) {
115 $this->repository = $repository;
116 $this->builder = $builder;
117 $this->hierarchy_repository = $hierarchy_repository;
118 $this->link_builder = $link_builder;
119 $this->author_archive = $author_archive;
120 $this->indexable_helper = $indexable_helper;
121 $this->post = $post;
122 $this->logger = $logger;
123 }
124
125 /**
126 * Initializes the integration.
127 *
128 * This is the place to register hooks and filters.
129 *
130 * @return void
131 */
132 public function register_hooks() {
133 \add_action( 'wp_insert_post', [ $this, 'build_indexable' ], \PHP_INT_MAX );
134 \add_action( 'delete_post', [ $this, 'delete_indexable' ] );
135
136 \add_action( 'edit_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
137 \add_action( 'add_attachment', [ $this, 'build_indexable' ], \PHP_INT_MAX );
138 \add_action( 'delete_attachment', [ $this, 'delete_indexable' ] );
139 }
140
141 /**
142 * Deletes the meta when a post is deleted.
143 *
144 * @param int $post_id Post ID.
145 *
146 * @return void
147 */
148 public function delete_indexable( $post_id ) {
149 $indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );
150
151 // Only interested in post indexables.
152 if ( ! $indexable || $indexable->object_type !== 'post' ) {
153 return;
154 }
155
156 $this->update_relations( $this->post->get_post( $post_id ) );
157
158 $this->update_has_public_posts( $indexable );
159
160 $this->hierarchy_repository->clear_ancestors( $indexable->id );
161 $this->link_builder->delete( $indexable );
162 $indexable->delete();
163 \do_action( 'wpseo_indexable_deleted', $indexable );
164 }
165
166 /**
167 * Updates the relations when the post indexable is built.
168 *
169 * @param Indexable $indexable The indexable.
170 * @param WP_Post $post The post.
171 *
172 * @return void
173 */
174 public function updated_indexable( $indexable, $post ) {
175 // Only interested in post indexables.
176 if ( $indexable->object_type !== 'post' ) {
177 return;
178 }
179
180 if ( \is_a( $post, Indexable::class ) ) {
181 \_deprecated_argument( __FUNCTION__, '17.7', 'The $old_indexable argument has been deprecated.' );
182 $post = $this->post->get_post( $indexable->object_id );
183 }
184
185 $this->update_relations( $post );
186 }
187
188 /**
189 * Saves post meta.
190 *
191 * @param int $post_id Post ID.
192 *
193 * @return void
194 */
195 public function build_indexable( $post_id ) {
196 // Bail if this is a multisite installation and the site has been switched.
197 if ( $this->is_multisite_and_switched() ) {
198 return;
199 }
200
201 try {
202 $indexable = $this->repository->find_by_id_and_type( $post_id, 'post', false );
203 $indexable = $this->builder->build_for_id_and_type( $post_id, 'post', $indexable );
204
205 $post = $this->post->get_post( $post_id );
206
207 /*
208 * Update whether an author has public posts.
209 * For example this post could be set to Draft or Private,
210 * which can influence if its author has any public posts at all.
211 */
212 if ( $indexable ) {
213 $this->update_has_public_posts( $indexable );
214 }
215
216 // Build links for this post.
217 if ( $post && $indexable && \in_array( $post->post_status, $this->post->get_public_post_statuses(), true ) ) {
218 $this->link_builder->build( $indexable, $post->post_content );
219 // Save indexable to persist the updated link count.
220 $this->indexable_helper->save_indexable( $indexable );
221 $this->updated_indexable( $indexable, $post );
222 }
223 } catch ( Exception $exception ) {
224 $this->logger->log( LogLevel::ERROR, $exception->getMessage() );
225 }
226 }
227
228 /**
229 * Updates the has_public_posts when the post indexable is built.
230 *
231 * @param Indexable $indexable The indexable to check.
232 *
233 * @return void
234 */
235 protected function update_has_public_posts( $indexable ) {
236 // Update the author indexable's has public posts value.
237 try {
238 $author_indexable = $this->repository->find_by_id_and_type( $indexable->author_id, 'user' );
239 if ( $author_indexable ) {
240 $author_indexable->has_public_posts = $this->author_archive->author_has_public_posts( $author_indexable->object_id );
241 $this->indexable_helper->save_indexable( $author_indexable );
242
243 if ( $this->indexable_helper->should_index_indexable( $author_indexable ) ) {
244 $this->reschedule_cleanup_if_author_has_no_posts( $author_indexable );
245 }
246 }
247 } catch ( Exception $exception ) {
248 $this->logger->log( LogLevel::ERROR, $exception->getMessage() );
249 }
250
251 // Update possible attachment's has public posts value.
252 $this->post->update_has_public_posts_on_attachments( $indexable->object_id, $indexable->is_public );
253 }
254
255 /**
256 * Reschedule indexable cleanup if the author does not have any public posts.
257 * This should remove the author from the indexable table, since we do not
258 * want to store authors without public facing posts in the table.
259 *
260 * @param Indexable $author_indexable The author indexable.
261 *
262 * @return void
263 */
264 protected function reschedule_cleanup_if_author_has_no_posts( $author_indexable ) {
265 if ( $author_indexable->has_public_posts === false ) {
266 $cleanup_not_yet_scheduled = ! \wp_next_scheduled( Cleanup_Integration::START_HOOK );
267 if ( $cleanup_not_yet_scheduled ) {
268 \wp_schedule_single_event( ( \time() + ( \MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
269 }
270 }
271 }
272
273 /**
274 * Updates the relations on post save or post status change.
275 *
276 * @param WP_Post $post The post that has been updated.
277 *
278 * @return void
279 */
280 protected function update_relations( $post ) {
281 $related_indexables = $this->get_related_indexables( $post );
282
283 foreach ( $related_indexables as $indexable ) {
284 // Ignore everything that is not an actual indexable.
285 if ( \is_a( $indexable, Indexable::class ) ) {
286 $indexable->object_last_modified = \max( $indexable->object_last_modified, $post->post_modified_gmt );
287 $this->indexable_helper->save_indexable( $indexable );
288 }
289 }
290 }
291
292 /**
293 * Retrieves the related indexables for given post.
294 *
295 * @param WP_Post $post The post to get the indexables for.
296 *
297 * @return Indexable[] The indexables.
298 */
299 protected function get_related_indexables( $post ) {
300 /**
301 * The related indexables.
302 *
303 * @var Indexable[] $related_indexables
304 */
305 $related_indexables = [];
306 $related_indexables[] = $this->repository->find_by_id_and_type( $post->post_author, 'user', false );
307 $related_indexables[] = $this->repository->find_for_post_type_archive( $post->post_type, false );
308 $related_indexables[] = $this->repository->find_for_home_page( false );
309
310 $taxonomies = \get_post_taxonomies( $post->ID );
311 $taxonomies = \array_filter( $taxonomies, 'is_taxonomy_viewable' );
312 $term_ids = [];
313 foreach ( $taxonomies as $taxonomy ) {
314 $terms = \get_the_terms( $post->ID, $taxonomy );
315
316 if ( empty( $terms ) || \is_wp_error( $terms ) ) {
317 continue;
318 }
319
320 $term_ids = \array_merge( $term_ids, \wp_list_pluck( $terms, 'term_id' ) );
321 }
322 $related_indexables = \array_merge(
323 $related_indexables,
324 $this->repository->find_by_multiple_ids_and_type( $term_ids, 'term', false ),
325 );
326
327 return \array_filter( $related_indexables );
328 }
329
330 /**
331 * Tests if the site is multisite and switched.
332 *
333 * @return bool True when the site is multisite and switched
334 */
335 protected function is_multisite_and_switched() {
336 return \is_multisite() && \ms_is_switched();
337 }
338 }
339