PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.6
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.6
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 / cleanup-integration.php

cleanup-integration.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.6, at src/integrations/cleanup-integration.php

318 lines 9.5 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;
4
5 use Closure;
6 use Yoast\WP\Lib\Model;
7
8 /**
9 * Adds cleanup hooks.
10 */
11 class Cleanup_Integration implements Integration_Interface {
12
13 /**
14 * Identifier used to determine the current task.
15 */
16 const CURRENT_TASK_OPTION = 'wpseo-cleanup-current-task';
17
18 /**
19 * Identifier for the cron job.
20 */
21 const CRON_HOOK = 'wpseo_cleanup_cron';
22
23 /**
24 * Identifier for starting the cleanup.
25 */
26 const START_HOOK = 'wpseo_start_cleanup_indexables';
27
28 /**
29 * Initializes the integration.
30 *
31 * This is the place to register hooks and filters.
32 *
33 * @return void
34 */
35 public function register_hooks() {
36 \add_action( self::START_HOOK, [ $this, 'run_cleanup' ] );
37 \add_action( self::CRON_HOOK, [ $this, 'run_cleanup_cron' ] );
38 \add_action( 'wpseo_deactivate', [ $this, 'reset_cleanup' ] );
39 }
40
41 /**
42 * Returns the conditionals based on which this loadable should be active.
43 *
44 * @return array The array of conditionals.
45 */
46 public static function get_conditionals() {
47 return [];
48 }
49
50 /**
51 * Starts the indexables cleanup.
52 *
53 * @return void
54 */
55 public function run_cleanup() {
56 $this->reset_cleanup();
57
58 $cleanups = $this->get_cleanup_tasks();
59 $limit = $this->get_limit();
60
61 foreach ( $cleanups as $name => $action ) {
62 $items_cleaned = $action( $limit );
63
64 if ( $items_cleaned === false ) {
65 return;
66 }
67
68 if ( $items_cleaned < $limit ) {
69 continue;
70 }
71
72 // There are more items to delete for the current cleanup job, start a cronjob at the specified job.
73 $this->start_cron_job( $name );
74 return;
75 }
76 }
77
78 /**
79 * Returns an array of cleanup tasks.
80 *
81 * @return Closure[] The cleanup tasks.
82 */
83 protected function get_cleanup_tasks() {
84 return \array_merge(
85 [
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 );
88 },
89 'clean_indexables_by_post_status_auto-draft' => function( $limit ) {
90 return $this->clean_indexables_with_post_status( 'auto-draft', $limit );
91 },
92 ],
93 $this->get_additional_tasks(),
94 [
95 /* 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 );
98 },
99 'clean_orphaned_content_seo_links_indexable_id' => function( $limit ) {
100 return $this->cleanup_orphaned_from_table( 'SEO_Links', 'indexable_id', $limit );
101 },
102 'clean_orphaned_content_seo_links_target_indexable_id' => function( $limit ) {
103 return $this->cleanup_orphaned_from_table( 'SEO_Links', 'target_indexable_id', $limit );
104 },
105 ]
106 );
107 }
108
109 /**
110 * Gets additional tasks from the 'wpseo_cleanup_tasks' filter.
111 *
112 * @return Closure[] Associative array of cleanup functions.
113 */
114 private function get_additional_tasks() {
115
116 /**
117 * Filter: Adds the possibility to add addition cleanup functions.
118 *
119 * @api array Associative array with unique keys. Value should be a cleanup function that receives a limit.
120 */
121 $additional_tasks = \apply_filters( 'wpseo_cleanup_tasks', [] );
122
123 if ( ! \is_array( $additional_tasks ) ) {
124 return [];
125 }
126
127 foreach ( $additional_tasks as $key => $value ) {
128 if ( \is_int( $key ) ) {
129 return [];
130 }
131 if ( ( ! \is_object( $value ) ) || ! ( $value instanceof Closure ) ) {
132 return [];
133 }
134 }
135
136 return $additional_tasks;
137 }
138
139 /**
140 * Gets the deletion limit for cleanups.
141 *
142 * @return int The limit for the amount of entities to be cleaned.
143 */
144 private function get_limit() {
145 /**
146 * Filter: Adds the possibility to limit the number of items that are deleted from the database on cleanup.
147 *
148 * @api int $limit Maximum number of indexables to be cleaned up per query.
149 */
150 $limit = \apply_filters( 'wpseo_cron_query_limit_size', 1000 );
151
152 if ( ! \is_int( $limit ) ) {
153 $limit = 1000;
154 }
155
156 return \abs( $limit );
157 }
158
159 /**
160 * Resets and stops the cleanup integration.
161 *
162 * @return void
163 */
164 public function reset_cleanup() {
165 \delete_option( self::CURRENT_TASK_OPTION );
166 \wp_unschedule_hook( self::CRON_HOOK );
167 }
168
169 /**
170 * Starts the cleanup cron job.
171 *
172 * @param string $task_name The task name of the next cleanup task to run.
173 *
174 * @return void
175 */
176 private function start_cron_job( $task_name ) {
177 \update_option( self::CURRENT_TASK_OPTION, $task_name );
178 \wp_schedule_event(
179 ( \time() + \HOUR_IN_SECONDS ),
180 'hourly',
181 self::CRON_HOOK
182 );
183 }
184
185 /**
186 * The callback that is called for the cleanup cron job.
187 *
188 * @return void
189 */
190 public function run_cleanup_cron() {
191 $current_task_name = \get_option( self::CURRENT_TASK_OPTION );
192
193 if ( $current_task_name === false ) {
194 $this->reset_cleanup();
195 return;
196 }
197
198 $limit = $this->get_limit();
199 $tasks = $this->get_cleanup_tasks();
200
201 // The task may have been added by a filter that has been removed, in that case just start over.
202 if ( ! isset( $tasks[ $current_task_name ] ) ) {
203 $current_task_name = \key( $tasks );
204 }
205
206 $current_task = \current( $tasks );
207 while ( $current_task !== false ) {
208 // Skip the tasks that have already been done.
209 if ( \key( $tasks ) !== $current_task_name ) {
210 $current_task = \next( $tasks );
211 continue;
212 }
213
214 // Call the cleanup callback function that accompanies the current task.
215 $items_cleaned = $current_task( $limit );
216
217 if ( $items_cleaned === false ) {
218 $this->reset_cleanup();
219 return;
220 }
221
222 if ( $items_cleaned === 0 ) {
223 // Check if we are finished with all tasks.
224 if ( \next( $tasks ) === false ) {
225 $this->reset_cleanup();
226 return;
227 }
228
229 // Continue with the next task next time the cron job is run.
230 \update_option( self::CURRENT_TASK_OPTION, \key( $tasks ) );
231 return;
232 }
233 // There were items deleted for the current task, continue with the same task next cron call.
234 return;
235 }
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 }
317 }
318