PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
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
← All changes | src/integrations/cleanup-integration.php +130 -107 18.228.5 View file →
@@ -2,9 +2,10 @@
2 2
3 3 namespace Yoast\WP\SEO\Integrations;
4 4
5 5 use Closure;
6 -use Yoast\WP\Lib\Model;
6 +use Yoast\WP\SEO\Helpers\Indexable_Helper;
7 +use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;
7 8
8 9 /**
9 10 * Adds cleanup hooks.
10 11 */
@@ -12,21 +13,49 @@
12 13
13 14 /**
14 15 * Identifier used to determine the current task.
15 16 */
16 - const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';
17 + public const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';
17 18
18 19 /**
19 20 * Identifier for the cron job.
20 21 */
21 - const CRON_HOOK = 'wpseo_cleanup_cron';
22 + public const CRON_HOOK = 'wpseo_cleanup_cron';
22 23
23 24 /**
24 25 * Identifier for starting the cleanup.
25 26 */
26 - const START_HOOK = 'wpseo_start_cleanup_indexables';
27 + public const START_HOOK = 'wpseo_start_cleanup_indexables';
27 28
28 29 /**
30 + * The indexable helper.
31 + *
32 + * @var Indexable_Helper
33 + */
34 + private $indexable_helper;
35 +
36 + /**
37 + * The cleanup repository.
38 + *
39 + * @var Indexable_Cleanup_Repository
40 + */
41 + private $cleanup_repository;
42 +
43 + /**
44 + * The constructor.
45 + *
46 + * @param Indexable_Cleanup_Repository $cleanup_repository The cleanup repository.
47 + * @param Indexable_Helper $indexable_helper The indexable helper.
48 + */
49 + public function __construct(
50 + Indexable_Cleanup_Repository $cleanup_repository,
51 + Indexable_Helper $indexable_helper
52 + ) {
53 + $this->cleanup_repository = $cleanup_repository;
54 + $this->indexable_helper = $indexable_helper;
55 + }
56 +
57 + /**
29 58 * Initializes the integration.
30 59 *
31 60 * This is the place to register hooks and filters.
32 61 *
@@ -40,9 +69,9 @@
40 69
41 70 /**
42 71 * Returns the conditionals based on which this loadable should be active.
43 72 *
44 - * @return array The array of conditionals.
73 + * @return array<string> The array of conditionals.
45 74 */
46 75 public static function get_conditionals() {
47 76 return [];
48 77 }
@@ -54,8 +83,13 @@
54 83 */
55 84 public function run_cleanup() {
56 85 $this->reset_cleanup();
57 86
87 + if ( ! $this->indexable_helper->should_index_indexables() ) {
88 + \wp_unschedule_hook( self::START_HOOK );
89 + return;
90 + }
91 +
58 92 $cleanups = $this->get_cleanup_tasks();
59 93 $limit = $this->get_limit();
60 94
61 95 foreach ( $cleanups as $name => $action ) {
@@ -70,8 +104,9 @@
70 104 }
71 105
72 106 // There are more items to delete for the current cleanup job, start a cronjob at the specified job.
73 107 $this->start_cron_job( $name );
108 +
74 109 return;
75 110 }
76 111 }
77 112
@@ -79,31 +114,59 @@
79 114 * Returns an array of cleanup tasks.
80 115 *
81 116 * @return Closure[] The cleanup tasks.
82 117 */
83 - protected function get_cleanup_tasks() {
118 + public function get_cleanup_tasks() {
84 119 return \array_merge(
85 120 [
86 - 'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function( $limit ) {
87 - return $this->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit );
121 + 'clean_indexables_with_object_type_and_object_sub_type_shop_order' => function ( $limit ) {
122 + return $this->cleanup_repository->clean_indexables_with_object_type_and_object_sub_type( 'post', 'shop_order', $limit );
88 123 },
89 - 'clean_indexables_by_post_status_auto-draft' => function( $limit ) {
90 - return $this->clean_indexables_with_post_status( 'auto-draft', $limit );
124 + 'clean_indexables_by_post_status_auto-draft' => function ( $limit ) {
125 + return $this->cleanup_repository->clean_indexables_with_post_status( 'auto-draft', $limit );
91 126 },
127 + 'clean_indexables_for_non_publicly_viewable_post' => function ( $limit ) {
128 + return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post( $limit );
129 + },
130 + 'clean_indexables_for_non_publicly_viewable_taxonomies' => function ( $limit ) {
131 + return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_taxonomies( $limit );
132 + },
133 + 'clean_indexables_for_non_publicly_viewable_post_type_archive_pages' => function ( $limit ) {
134 + return $this->cleanup_repository->clean_indexables_for_non_publicly_viewable_post_type_archive_pages( $limit );
135 + },
136 + 'clean_indexables_for_authors_archive_disabled' => function ( $limit ) {
137 + return $this->cleanup_repository->clean_indexables_for_authors_archive_disabled( $limit );
138 + },
139 + 'clean_indexables_for_authors_without_archive' => function ( $limit ) {
140 + return $this->cleanup_repository->clean_indexables_for_authors_without_archive( $limit );
141 + },
142 + 'update_indexables_author_to_reassigned' => function ( $limit ) {
143 + return $this->cleanup_repository->update_indexables_author_to_reassigned( $limit );
144 + },
145 + 'clean_orphaned_user_indexables_without_wp_user' => function ( $limit ) {
146 + return $this->cleanup_repository->clean_indexables_for_orphaned_users( $limit );
147 + },
148 + 'clean_orphaned_user_indexables_without_wp_post' => function ( $limit ) {
149 + return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'posts', 'ID', 'post', $limit );
150 + },
151 + 'clean_orphaned_user_indexables_without_wp_term' => function ( $limit ) {
152 + return $this->cleanup_repository->clean_indexables_for_object_type_and_source_table( 'terms', 'term_id', 'term', $limit );
153 + },
92 154 ],
93 - $this->get_additional_tasks(),
155 + $this->get_additional_indexable_cleanups(),
94 156 [
95 157 /* These should always be the last ones to be called. */
96 - 'clean_orphaned_content_indexable_hierarchy' => function( $limit ) {
97 - return $this->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit );
158 + 'clean_orphaned_content_indexable_hierarchy' => function ( $limit ) {
159 + return $this->cleanup_repository->cleanup_orphaned_from_table( 'Indexable_Hierarchy', 'indexable_id', $limit );
98 160 },
99 - 'clean_orphaned_content_seo_links_indexable_id' => function( $limit ) {
100 - return $this->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit );
161 + 'clean_orphaned_content_seo_links_indexable_id' => function ( $limit ) {
162 + return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit );
101 163 },
102 - 'clean_orphaned_content_seo_links_target_indexable_id' => function( $limit ) {
103 - return $this->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit );
164 + 'clean_orphaned_content_seo_links_target_indexable_id' => function ( $limit ) {
165 + return $this->cleanup_repository->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit );
104 166 },
105 - ]
167 + ],
168 + $this->get_additional_misc_cleanups(),
106 169 );
107 170 }
108 171
109 172 /**
@@ -108,19 +171,47 @@
108 171
109 172 /**
110 173 * Gets additional tasks from the 'wpseo_cleanup_tasks' filter.
111 174 *
112 - * @return Closure[] Associative array of cleanup functions.
175 + * @return Closure[] Associative array of indexable cleanup functions.
113 176 */
114 - private function get_additional_tasks() {
177 + private function get_additional_indexable_cleanups() {
115 178
116 179 /**
117 - * Filter: Adds the possibility to add addition cleanup functions.
180 + * Filter: Adds the possibility to add additional indexable cleanup functions.
118 181 *
119 - * @api array Associative array with unique keys. Value should be a cleanup function that receives a limit.
182 + * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit.
120 183 */
121 184 $additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] );
122 185
186 + return $this->validate_additional_tasks( $additional_tasks );
187 + }
188 +
189 + /**
190 + * Gets additional tasks from the 'wpseo_misc_cleanup_tasks' filter.
191 + *
192 + * @return Closure[] Associative array of indexable cleanup functions.
193 + */
194 + private function get_additional_misc_cleanups() {
195 +
196 + /**
197 + * Filter: Adds the possibility to add additional non-indexable cleanup functions.
198 + *
199 + * @param array $additional_tasks Associative array with unique keys. Value should be a cleanup function that receives a limit.
200 + */
201 + $additional_tasks = \apply_filters( 'wpseo_misc_cleanup_tasks', [] );
202 +
203 + return $this->validate_additional_tasks( $additional_tasks );
204 + }
205 +
206 + /**
207 + * Validates the additional tasks.
208 + *
209 + * @param Closure[] $additional_tasks The additional tasks to validate.
210 + *
211 + * @return Closure[] The validated additional tasks.
212 + */
213 + private function validate_additional_tasks( $additional_tasks ) {
123 214 if ( ! \is_array( $additional_tasks ) ) {
124 215 return [];
125 216 }
126 217
@@ -144,9 +235,9 @@
144 235 private function get_limit() {
145 236 /**
146 237 * Filter: Adds the possibility to limit the number of items that are deleted from the database on cleanup.
147 238 *
148 - * @api int $limit Maximum number of indexables to be cleaned up per query.
239 + * @param int $limit Maximum number of indexables to be cleaned up per query.
149 240 */
150 241 $limit = \apply_filters( 'wpseo_cron_query_limit_size', 1000 );
151 242
152 243 if ( ! \is_int( $limit ) ) {
@@ -168,18 +259,19 @@
168 259
169 260 /**
170 261 * Starts the cleanup cron job.
171 262 *
172 - * @param string $task_name The task name of the next cleanup task to run.
263 + * @param string $task_name The task name of the next cleanup task to run.
264 + * @param int $schedule_time The time in seconds to wait before running the first cron job. Default is 1 hour.
173 265 *
174 266 * @return void
175 267 */
176 - private function start_cron_job( $task_name ) {
268 + public function start_cron_job( $task_name, $schedule_time = 3600 ) {
177 269 \update_option( self::CURRENT_TASK_OPTION, $task_name );
178 270 \wp_schedule_event(
179 - ( \time() + \HOUR_IN_SECONDS ),
271 + ( \time() + $schedule_time ),
180 272 'hourly',
181 - self::CRON_HOOK
273 + self::CRON_HOOK,
182 274 );
183 275 }
184 276
185 277 /**
@@ -187,12 +279,19 @@
187 279 *
188 280 * @return void
189 281 */
190 282 public function run_cleanup_cron() {
283 + if ( ! $this->indexable_helper->should_index_indexables() ) {
284 + $this->reset_cleanup();
285 +
286 + return;
287 + }
288 +
191 289 $current_task_name = \get_option( self::CURRENT_TASK_OPTION );
192 290
193 291 if ( $current_task_name === false ) {
194 292 $this->reset_cleanup();
293 +
195 294 return;
196 295 }
197 296
198 297 $limit = $this->get_limit();
@@ -215,8 +314,9 @@
215 314 $items_cleaned = $current_task( $limit );
216 315
217 316 if ( $items_cleaned === false ) {
218 317 $this->reset_cleanup();
318 +
219 319 return;
220 320 }
221 321
222 322 if ( $items_cleaned === 0 ) {
@@ -222,96 +322,19 @@
222 322 if ( $items_cleaned === 0 ) {
223 323 // Check if we are finished with all tasks.
224 324 if ( \next( $tasks ) === false ) {
225 325 $this->reset_cleanup();
326 +
226 327 return;
227 328 }
228 329
229 330 // Continue with the next task next time the cron job is run.
230 331 \update_option( self::CURRENT_TASK_OPTION, \key( $tasks ) );
332 +
231 333 return;
232 334 }
335 +
233 336 // There were items deleted for the current task, continue with the same task next cron call.
234 337 return;
235 338 }
236 - }
237 -
238 - /**
239 - * Deletes rows from the indexable table depending on the object_type and object_sub_type.
240 - *
241 - * @param string $object_type The object type to query.
242 - * @param string $object_sub_type The object subtype to query.
243 - * @param int $limit The limit we'll apply to the delete query.
244 - *
245 - * @return int|bool The number of rows that was deleted or false if the query failed.
246 - */
247 - protected function clean_indexables_with_object_type_and_object_sub_type( $object_type, $object_sub_type, $limit ) {
248 - global $wpdb;
249 -
250 - $indexable_table = Model::get_table_name( 'Indexable' );
251 -
252 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
253 - $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = %s AND object_sub_type = %s ORDER BY id LIMIT %d", $object_type, $object_sub_type, $limit );
254 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
255 - return $wpdb->query( $sql );
256 - }
257 -
258 - /**
259 - * Deletes rows from the indexable table depending on the post_status.
260 - *
261 - * @param string $post_status The post status to query.
262 - * @param int $limit The limit we'll apply to the delete query.
263 - *
264 - * @return int|bool The number of rows that was deleted or false if the query failed.
265 - */
266 - protected function clean_indexables_with_post_status( $post_status, $limit ) {
267 - global $wpdb;
268 -
269 - $indexable_table = Model::get_table_name( 'Indexable' );
270 -
271 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
272 - $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND post_status = %s ORDER BY id LIMIT %d", $post_status, $limit );
273 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
274 - return $wpdb->query( $sql );
275 - }
276 -
277 - /**
278 - * Cleans orphaned rows from a yoast table.
279 - *
280 - * @param string $table The table to clean up.
281 - * @param string $column The table column the cleanup will rely on.
282 - * @param int $limit The limit we'll apply to the queries.
283 - *
284 - * @return int|bool The number of deleted rows, false if the query fails.
285 - */
286 - protected function cleanup_orphaned_from_table( $table, $column, $limit ) {
287 - global $wpdb;
288 -
289 - $table = Model::get_table_name( $table );
290 - $indexable_table = Model::get_table_name( 'Indexable' );
291 -
292 - // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well.
293 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
294 - $query = $wpdb->prepare(
295 - "
296 - SELECT table_to_clean.{$column}
297 - FROM {$table} table_to_clean
298 - LEFT JOIN {$indexable_table} AS indexable_table
299 - ON table_to_clean.{$column} = indexable_table.id
300 - WHERE indexable_table.id IS NULL
301 - AND table_to_clean.{$column} IS NOT NULL
302 - LIMIT %d",
303 - $limit
304 - );
305 - // phpcs:enable
306 -
307 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
308 - $orphans = $wpdb->get_col( $query );
309 -
310 - if ( empty( $orphans ) ) {
311 - return 0;
312 - }
313 -
314 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
315 - return $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' )' );
316 339 }
317 340 }