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 | inc/class-upgrade.php +680 -67 18.228.5 View file →
@@ -5,8 +5,11 @@
5 5 * @package WPSEO\Internal
6 6 */
7 7
8 8 use Yoast\WP\Lib\Model;
9 +use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
10 +use Yoast\WP\SEO\Integrations\Cleanup_Integration;
11 +use Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher;
9 12
10 13 /**
11 14 * This code handles the option upgrades.
12 15 */
@@ -14,9 +17,9 @@
14 17
15 18 /**
16 19 * The taxonomy helper.
17 20 *
18 - * @var \Yoast\WP\SEO\Helpers\Taxonomy_Helper
21 + * @var Taxonomy_Helper
19 22 */
20 23 private $taxonomy_helper;
21 24
22 25 /**
@@ -75,12 +78,23 @@
75 78 '16.5-RC0' => 'upgrade_165',
76 79 '17.2-RC0' => 'upgrade_172',
77 80 '17.7.1-RC0' => 'upgrade_1771',
78 81 '17.9-RC0' => 'upgrade_179',
82 + '18.3-RC3' => 'upgrade_183',
83 + '18.6-RC0' => 'upgrade_186',
84 + '18.9-RC0' => 'upgrade_189',
85 + '19.1-RC0' => 'upgrade_191',
86 + '19.3-RC0' => 'upgrade_193',
87 + '19.6-RC0' => 'upgrade_196',
88 + '19.11-RC0' => 'upgrade_1911',
89 + '20.2-RC0' => 'upgrade_202',
90 + '20.5-RC0' => 'upgrade_205',
91 + '20.7-RC0' => 'upgrade_207',
92 + '20.8-RC0' => 'upgrade_208',
93 + '22.6-RC0' => 'upgrade_226',
79 94 ];
80 95
81 96 array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version );
82 -
83 97 if ( version_compare( $version, '12.5-RC0', '<' ) ) {
84 98 /*
85 99 * We have to run this by hook, because otherwise:
86 100 * - the theme support check isn't available.
@@ -88,16 +102,12 @@
88 102 */
89 103 add_action( 'init', [ $this, 'upgrade_125' ] );
90 104 }
91 105
92 - // Since 3.7.
93 - $upsell_notice = new WPSEO_Product_Upsell_Notice();
94 - $upsell_notice->set_upgrade_notice();
95 -
96 106 /**
97 107 * Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO.
98 108 *
99 - * @api string - The current version of Yoast SEO
109 + * @param string $version The current version of Yoast SEO
100 110 */
101 111 do_action( 'wpseo_run_upgrade', $version );
102 112
103 113 $this->finish_up( $version );
@@ -122,8 +132,10 @@
122 132 * Adds a new upgrade history entry.
123 133 *
124 134 * @param string $current_version The old version from which we are upgrading.
125 135 * @param string $new_version The version we are upgrading to.
136 + *
137 + * @return void
126 138 */
127 139 protected function add_upgrade_history( $current_version, $new_version ) {
128 140 $upgrade_history = new WPSEO_Upgrade_History();
129 141 $upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) );
@@ -137,11 +149,13 @@
137 149 * @return void
138 150 */
139 151 protected function finish_up( $previous_version = null ) {
140 152 if ( $previous_version ) {
141 - WPSEO_Options::set( 'previous_version', $previous_version );
153 + WPSEO_Options::set( 'previous_version', $previous_version, 'wpseo' );
154 + // Store timestamp when plugin is updated from a previous version.
155 + WPSEO_Options::set( 'last_updated_on', time(), 'wpseo' );
142 156 }
143 - WPSEO_Options::set( 'version', WPSEO_VERSION );
157 + WPSEO_Options::set( 'version', WPSEO_VERSION, 'wpseo' );
144 158
145 159 // Just flush rewrites, always, to at least make them work after an upgrade.
146 160 add_action( 'shutdown', 'flush_rewrite_rules' );
147 161
@@ -155,8 +169,10 @@
155 169 /**
156 170 * Run the Yoast SEO 1.5 upgrade routine.
157 171 *
158 172 * @param string $version Current plugin version.
173 + *
174 + * @return void
159 175 */
160 176 private function upgrade_15( $version ) {
161 177 // Clean up options and meta.
162 178 WPSEO_Options::clean_up( null, $version );
@@ -164,8 +180,10 @@
164 180 }
165 181
166 182 /**
167 183 * Moves options that moved position in WPSEO 2.0.
184 + *
185 + * @return void
168 186 */
169 187 private function upgrade_20() {
170 188 /**
171 189 * Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table.
@@ -181,8 +199,10 @@
181 199 }
182 200
183 201 /**
184 202 * Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly.
203 + *
204 + * @return void
185 205 */
186 206 private function upgrade_21() {
187 207 $taxonomies = get_option( 'wpseo_taxonomy_meta', [] );
188 208
@@ -204,8 +224,10 @@
204 224 }
205 225
206 226 /**
207 227 * Performs upgrade functions to Yoast SEO 2.2.
228 + *
229 + * @return void
208 230 */
209 231 private function upgrade_22() {
210 232 // Unschedule our tracking.
211 233 wp_clear_scheduled_hook( 'yoast_tracking' );
@@ -214,8 +236,10 @@
214 236 }
215 237
216 238 /**
217 239 * Schedules upgrade function to Yoast SEO 2.3.
240 + *
241 + * @return void
218 242 */
219 243 private function upgrade_23() {
220 244 add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 );
221 245 add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 );
@@ -222,10 +246,14 @@
222 246 }
223 247
224 248 /**
225 249 * Performs upgrade query to Yoast SEO 2.3.
250 + *
251 + * @return void
226 252 */
227 253 public function upgrade_23_query() {
254 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: executed only during the upgrade routine.
255 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Reason: executed only during the upgrade routine.
228 256 $wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' );
229 257
230 258 if ( ! empty( $wp_query->posts ) ) {
231 259 $options = get_option( 'wpseo_xml' );
@@ -253,8 +281,10 @@
253 281 }
254 282
255 283 /**
256 284 * Performs upgrade functions to Yoast SEO 3.0.
285 + *
286 + * @return void
257 287 */
258 288 private function upgrade_30() {
259 289 // Remove the meta fields for sitemap prio.
260 290 delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' );
@@ -261,8 +291,10 @@
261 291 }
262 292
263 293 /**
264 294 * Performs upgrade functions to Yoast SEO 3.3.
295 + *
296 + * @return void
265 297 */
266 298 private function upgrade_33() {
267 299 // Notification dismissals have been moved to User Meta instead of global option.
268 300 delete_option( Yoast_Notification_Center::STORAGE_KEY );
@@ -269,18 +301,29 @@
269 301 }
270 302
271 303 /**
272 304 * Performs upgrade functions to Yoast SEO 3.6.
305 + *
306 + * @return void
273 307 */
274 - private function upgrade_36() {
308 + protected function upgrade_36() {
275 309 global $wpdb;
276 310
277 311 // Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled.
278 - $wpdb->query( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "wpseo_sitemap_%" AND autoload = "yes"' );
312 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
313 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
314 + $wpdb->query(
315 + $wpdb->prepare(
316 + 'DELETE FROM %i WHERE %i LIKE %s AND autoload IN ("on", "yes")',
317 + [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ],
318 + ),
319 + );
279 320 }
280 321
281 322 /**
282 323 * Removes the about notice when its still in the database.
324 + *
325 + * @return void
283 326 */
284 327 private function upgrade_40() {
285 328 $center = Yoast_Notification_Center::get();
286 329 $center->remove_notification_by_id( 'wpseo-dismiss-about' );
@@ -287,8 +330,10 @@
287 330 }
288 331
289 332 /**
290 333 * Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo.
334 + *
335 + * @return void
291 336 */
292 337 private function upgrade_44() {
293 338 $wpseo_titles = $this->get_option_from_database( 'wpseo_titles' );
294 339
@@ -300,25 +345,31 @@
300 345 }
301 346
302 347 /**
303 348 * Renames the meta name for the cornerstone content. It was a public meta field and it has to be private.
349 + *
350 + * @return void
304 351 */
305 352 private function upgrade_47() {
306 353 global $wpdb;
307 354
308 355 // The meta key has to be private, so prefix it.
356 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
357 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
309 358 $wpdb->query(
310 359 $wpdb->prepare(
311 360 'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"',
312 - WPSEO_Cornerstone_Filter::META_NAME
313 - )
361 + WPSEO_Cornerstone_Filter::META_NAME,
362 + ),
314 363 );
315 364 }
316 365
317 366 /**
318 367 * Removes the 'wpseo-dismiss-about' notice for every user that still has it.
368 + *
369 + * @return void
319 370 */
320 - private function upgrade_49() {
371 + protected function upgrade_49() {
321 372 global $wpdb;
322 373
323 374 /*
324 375 * Using a filter to remove the notification for the current logged in user. The notification center is
@@ -329,19 +380,28 @@
329 380 add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] );
330 381
331 382 $meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY;
332 383
384 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
385 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
333 386 $usermetas = $wpdb->get_results(
334 387 $wpdb->prepare(
335 388 '
336 - SELECT user_id, meta_value
337 - FROM ' . $wpdb->usermeta . '
338 - WHERE meta_key = %s AND meta_value LIKE %s
389 + SELECT %i, %i
390 + FROM %i
391 + WHERE %i = %s AND %i LIKE %s
339 392 ',
340 - $meta_key,
341 - '%wpseo-dismiss-about%'
393 + [
394 + 'user_id',
395 + 'meta_value',
396 + $wpdb->usermeta,
397 + 'meta_key',
398 + $meta_key,
399 + 'meta_value',
400 + '%wpseo-dismiss-about%',
401 + ],
342 402 ),
343 - ARRAY_A
403 + ARRAY_A,
344 404 );
345 405
346 406 if ( empty( $usermetas ) ) {
347 407 return;
@@ -378,18 +438,30 @@
378 438 }
379 439
380 440 /**
381 441 * Adds the yoast_seo_links table to the database.
442 + *
443 + * @return void
382 444 */
383 - private function upgrade_50() {
445 + protected function upgrade_50() {
384 446 global $wpdb;
385 447
386 448 // Deletes the post meta value, which might created in the RC.
387 - $wpdb->query( 'DELETE FROM ' . $wpdb->postmeta . ' WHERE meta_key = "_yst_content_links_processed"' );
449 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
450 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
451 + $wpdb->query(
452 + $wpdb->prepare(
453 + "DELETE FROM %i
454 + WHERE %i = '_yst_content_links_processed'",
455 + [ $wpdb->postmeta, 'meta_key' ],
456 + ),
457 + );
388 458 }
389 459
390 460 /**
391 461 * Register new capabilities and roles.
462 + *
463 + * @return void
392 464 */
393 465 private function upgrade_55() {
394 466 // Register roles.
395 467 do_action( 'wpseo_register_roles' );
@@ -428,9 +500,8 @@
428 500
429 501 // Move one XML sitemap setting, then delete the option.
430 502 $this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' );
431 503
432 -
433 504 // Move the RSS settings to the search appearance settings, then delete the RSS option.
434 505 $this->save_option_setting( $wpseo_rss, 'rssbefore' );
435 506 $this->save_option_setting( $wpseo_rss, 'rssafter' );
436 507
@@ -475,8 +546,10 @@
475 546 $yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' );
476 547
477 548 // Moves the user meta for excluding from the XML sitemap to a noindex.
478 549 global $wpdb;
550 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
551 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
479 552 $wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" );
480 553 }
481 554
482 555 /**
@@ -504,8 +577,10 @@
504 577 */
505 578 private function upgrade_73() {
506 579 global $wpdb;
507 580 // We've moved the cornerstone checkbox to our proper namespace.
581 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
582 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
508 583 $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" );
509 584
510 585 // Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2.
511 586 delete_option( 'whip_dismiss_timestamp' );
@@ -515,9 +590,9 @@
515 590 * Performs the 7.4 upgrade.
516 591 *
517 592 * @return void
518 593 */
519 - private function upgrade_74() {
594 + protected function upgrade_74() {
520 595 $this->remove_sitemap_validators();
521 596 }
522 597
523 598 /**
@@ -567,9 +642,9 @@
567 642 * Performs the 9.0 upgrade.
568 643 *
569 644 * @return void
570 645 */
571 - private function upgrade_90() {
646 + protected function upgrade_90() {
572 647 global $wpdb;
573 648
574 649 // Invalidate all sitemap cache transients.
575 650 WPSEO_Sitemaps_Cache_Validator::cleanup_database();
@@ -576,9 +651,17 @@
576 651
577 652 // Removes all scheduled tasks for hitting the sitemap index.
578 653 wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' );
579 654
580 - $wpdb->query( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE "wpseo_sitemap_%"' );
655 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
656 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
657 + $wpdb->query(
658 + $wpdb->prepare(
659 + 'DELETE FROM %i
660 + WHERE %i LIKE %s',
661 + [ $wpdb->options, 'option_name', 'wpseo_sitemap_%' ],
662 + ),
663 + );
581 664 }
582 665
583 666 /**
584 667 * Performs the 10.0 upgrade.
@@ -611,8 +694,10 @@
611 694 /**
612 695 * Performs the 12.3 upgrade.
613 696 *
614 697 * Removes the about notice when its still in the database.
698 + *
699 + * @return void
615 700 */
616 701 private function upgrade_123() {
617 702 $plugins = [
618 703 'yoast-seo-premium',
@@ -632,8 +717,10 @@
632 717 /**
633 718 * Performs the 12.4 upgrade.
634 719 *
635 720 * Removes the Google plus defaults from the database.
721 + *
722 + * @return void
636 723 */
637 724 private function upgrade_124() {
638 725 $this->cleanup_option_data( 'wpseo_social' );
639 726 }
@@ -639,8 +726,10 @@
639 726 }
640 727
641 728 /**
642 729 * Performs the 12.5 upgrade.
730 + *
731 + * @return void
643 732 */
644 733 public function upgrade_125() {
645 734 // Disables the force rewrite title when the theme supports it through WordPress.
646 735 if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) {
@@ -647,9 +736,17 @@
647 736 WPSEO_Options::set( 'forcerewritetitle', false );
648 737 }
649 738
650 739 global $wpdb;
651 - $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key = 'wp_yoast_promo_hide_premium_upsell_admin_block'" );
740 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
741 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
742 + $wpdb->query(
743 + $wpdb->prepare(
744 + 'DELETE FROM %i
745 + WHERE %i = %s',
746 + [ $wpdb->usermeta, 'meta_key', 'wp_yoast_promo_hide_premium_upsell_admin_block' ],
747 + ),
748 + );
652 749
653 750 // Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released.
654 751 $center = Yoast_Notification_Center::get();
655 752 $center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
@@ -656,8 +753,10 @@
656 753 }
657 754
658 755 /**
659 756 * Performs the 12.8 upgrade.
757 + *
758 + * @return void
660 759 */
661 760 private function upgrade_128() {
662 761 // Re-save wpseo to make sure bf_banner_2019_dismissed key is gone.
663 762 $this->cleanup_option_data( 'wpseo' );
@@ -667,8 +766,10 @@
667 766 }
668 767
669 768 /**
670 769 * Performs the 13.2 upgrade.
770 + *
771 + * @return void
671 772 */
672 773 private function upgrade_132() {
673 774 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-tagline-notice' );
674 775 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-permalink-notice' );
@@ -703,8 +804,10 @@
703 804 }
704 805
705 806 /**
706 807 * Perform the 14.0.3 upgrade.
808 + *
809 + * @return void
707 810 */
708 811 private function upgrade_1403() {
709 812 WPSEO_Options::set( 'ignore_indexation_warning', false );
710 813 }
@@ -710,8 +813,10 @@
710 813 }
711 814
712 815 /**
713 816 * Performs the 14.1 upgrade.
817 + *
818 + * @return void
714 819 */
715 820 private function upgrade_141() {
716 821 /*
717 822 * These notifications are retrieved from storage on the `init` hook with
@@ -726,8 +831,10 @@
726 831 /**
727 832 * Performs the 14.2 upgrade.
728 833 *
729 834 * Removes the yoast-acf-analysis notice when it's still in the database.
835 + *
836 + * @return void
730 837 */
731 838 private function upgrade_142() {
732 839 add_action( 'init', [ $this, 'remove_acf_notification_for_142' ] );
733 840 }
@@ -733,8 +840,10 @@
733 840 }
734 841
735 842 /**
736 843 * Performs the 14.5 upgrade.
844 + *
845 + * @return void
737 846 */
738 847 private function upgrade_145() {
739 848 add_action( 'init', [ $this, 'set_indexation_completed_option_for_145' ] );
740 849 }
@@ -740,8 +849,10 @@
740 849 }
741 850
742 851 /**
743 852 * Performs the 14.9 upgrade.
853 + *
854 + * @return void
744 855 */
745 856 private function upgrade_149() {
746 857 $version = get_option( 'wpseo_license_server_version', 2 );
747 858 WPSEO_Options::set( 'license_server_version', $version );
@@ -803,21 +914,25 @@
803 914 }
804 915
805 916 /**
806 917 * Performs the 15.9.1 upgrade routine.
918 + *
919 + * @return void
807 920 */
808 921 private function upgrade_1591() {
809 - $enabled_auto_updates = \get_option( 'auto_update_plugins' );
810 - $addon_update_watcher = YoastSEO()->classes->get( \Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher::class );
922 + $enabled_auto_updates = get_option( 'auto_update_plugins' );
923 + $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
811 924 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', [], $enabled_auto_updates );
812 925 }
813 926
814 927 /**
815 928 * Performs the 16.2 upgrade routine.
929 + *
930 + * @return void
816 931 */
817 932 private function upgrade_162() {
818 - $enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
819 - $addon_update_watcher = YoastSEO()->classes->get( \Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher::class );
933 + $enabled_auto_updates = get_site_option( 'auto_update_plugins' );
934 + $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
820 935 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
821 936 }
822 937
823 938 /**
@@ -832,32 +947,37 @@
832 947 add_action( 'init', [ $this, 'reset_og_settings_to_default_values' ], 100 );
833 948 }
834 949
835 950 /**
836 - * Performs the 17.2 upgrade. Cleans out any unnecessary indexables. See $cleanup_integration->get_cleanup_tasks() to see what will be cleaned out.
951 + * Performs the 17.2 upgrade. Cleans out any unnecessary indexables. See $cleanup_integration->get_cleanup_tasks()
952 + * to see what will be cleaned out.
837 953 *
838 954 * @return void
839 955 */
840 956 private function upgrade_172() {
841 - \wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' );
842 - \wp_unschedule_hook( 'wpseo_cleanup_indexables' );
957 + wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' );
958 + wp_unschedule_hook( 'wpseo_cleanup_indexables' );
843 959
844 - if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
845 - \wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK );
960 + if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
961 + wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
846 962 }
847 963 }
848 964
849 965 /**
850 966 * Performs the 17.7.1 upgrade routine.
967 + *
968 + * @return void
851 969 */
852 970 private function upgrade_1771() {
853 - $enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
854 - $addon_update_watcher = YoastSEO()->classes->get( \Yoast\WP\SEO\Integrations\Watchers\Addon_Update_Watcher::class );
971 + $enabled_auto_updates = get_site_option( 'auto_update_plugins' );
972 + $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
855 973 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
856 974 }
857 975
858 976 /**
859 977 * Performs the 17.9 upgrade routine.
978 + *
979 + * @return void
860 980 */
861 981 private function upgrade_179() {
862 982 WPSEO_Options::set( 'wincher_integration_active', true );
863 983 }
@@ -862,8 +982,184 @@
862 982 WPSEO_Options::set( 'wincher_integration_active', true );
863 983 }
864 984
865 985 /**
986 + * Performs the 18.3 upgrade routine.
987 + *
988 + * @return void
989 + */
990 + private function upgrade_183() {
991 + $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' );
992 + }
993 +
994 + /**
995 + * Performs the 18.6 upgrade routine.
996 + *
997 + * @return void
998 + */
999 + private function upgrade_186() {
1000 + if ( is_multisite() ) {
1001 + WPSEO_Options::set( 'allow_wincher_integration_active', false );
1002 + }
1003 + }
1004 +
1005 + /**
1006 + * Performs the 18.9 upgrade routine.
1007 + *
1008 + * @return void
1009 + */
1010 + private function upgrade_189() {
1011 + // Make old users not get the Installation Success page after upgrading.
1012 + WPSEO_Options::set( 'should_redirect_after_install_free', false );
1013 + // We're adding a hardcoded time here, so that in the future we can be able to identify whether the user did see the Installation Success page or not.
1014 + // If they did, they wouldn't have this hardcoded value in that option, but rather (roughly) the timestamp of the moment they saw it.
1015 + WPSEO_Options::set( 'activation_redirect_timestamp_free', 1_652_258_756 );
1016 +
1017 + // Transfer the Social URLs.
1018 + $other = [];
1019 + $other[] = WPSEO_Options::get( 'instagram_url' );
1020 + $other[] = WPSEO_Options::get( 'linkedin_url' );
1021 + $other[] = WPSEO_Options::get( 'myspace_url' );
1022 + $other[] = WPSEO_Options::get( 'pinterest_url' );
1023 + $other[] = WPSEO_Options::get( 'youtube_url' );
1024 + $other[] = WPSEO_Options::get( 'wikipedia_url' );
1025 +
1026 + WPSEO_Options::set( 'other_social_urls', array_values( array_unique( array_filter( $other ) ) ) );
1027 +
1028 + // Transfer the progress of the old Configuration Workout.
1029 + $workout_data = WPSEO_Options::get( 'workouts_data' );
1030 + $old_conf_progress = ( $workout_data['configuration']['finishedSteps'] ?? [] );
1031 +
1032 + if ( in_array( 'optimizeSeoData', $old_conf_progress, true ) && in_array( 'siteRepresentation', $old_conf_progress, true ) ) {
1033 + // If completed ‘SEO optimization’ and ‘Site representation’ step, we assume the workout was completed.
1034 + $configuration_finished_steps = [
1035 + 'siteRepresentation',
1036 + 'socialProfiles',
1037 + 'personalPreferences',
1038 + ];
1039 + WPSEO_Options::set( 'configuration_finished_steps', $configuration_finished_steps );
1040 + }
1041 + }
1042 +
1043 + /**
1044 + * Performs the 19.1 upgrade routine.
1045 + *
1046 + * @return void
1047 + */
1048 + private function upgrade_191() {
1049 + if ( is_multisite() ) {
1050 + WPSEO_Options::set( 'allow_remove_feed_post_comments', true );
1051 + }
1052 + }
1053 +
1054 + /**
1055 + * Performs the 19.3 upgrade routine.
1056 + *
1057 + * @return void
1058 + */
1059 + private function upgrade_193() {
1060 + if ( empty( get_option( 'wpseo_premium', [] ) ) ) {
1061 + WPSEO_Options::set( 'enable_index_now', true );
1062 + WPSEO_Options::set( 'enable_link_suggestions', true );
1063 + }
1064 + }
1065 +
1066 + /**
1067 + * Performs the 19.6 upgrade routine.
1068 + *
1069 + * @return void
1070 + */
1071 + private function upgrade_196() {
1072 + WPSEO_Options::set( 'ryte_indexability', false );
1073 + WPSEO_Options::set( 'allow_ryte_indexability', false );
1074 + wp_clear_scheduled_hook( 'wpseo_ryte_fetch' );
1075 + }
1076 +
1077 + /**
1078 + * Performs the 19.11 upgrade routine.
1079 + *
1080 + * @return void
1081 + */
1082 + private function upgrade_1911() {
1083 + add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_post_types' ] );
1084 + add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_taxonomies' ] );
1085 + $this->deduplicate_unindexed_indexable_rows();
1086 + $this->remove_indexable_rows_for_disabled_authors_archive();
1087 + if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1088 + wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1089 + }
1090 + }
1091 +
1092 + /**
1093 + * Performs the 20.2 upgrade routine.
1094 + *
1095 + * @return void
1096 + */
1097 + private function upgrade_202() {
1098 + if ( WPSEO_Options::get( 'disable-attachment', true ) ) {
1099 + $attachment_cleanup_helper = YoastSEO()->helpers->attachment_cleanup;
1100 +
1101 + $attachment_cleanup_helper->remove_attachment_indexables( true );
1102 + $attachment_cleanup_helper->clean_attachment_links_from_target_indexable_ids( true );
1103 + }
1104 +
1105 + $this->clean_unindexed_indexable_rows_with_no_object_id();
1106 +
1107 + if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1108 + // This schedules the cleanup routine cron again, since in combination of premium cleans up the prominent words table. We also want to cleanup possible orphaned hierarchies from the above cleanups.
1109 + wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1110 + }
1111 + }
1112 +
1113 + /**
1114 + * Performs the 20.5 upgrade routine.
1115 + *
1116 + * @return void
1117 + */
1118 + private function upgrade_205() {
1119 + if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1120 + wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1121 + }
1122 + }
1123 +
1124 + /**
1125 + * Performs the 20.7 upgrade routine.
1126 + * Removes the metadata related to the settings page introduction modal for all the users.
1127 + * Also, schedules another cleanup scheduled action.
1128 + *
1129 + * @return void
1130 + */
1131 + private function upgrade_207() {
1132 + add_action( 'shutdown', [ $this, 'delete_user_introduction_meta' ] );
1133 + }
1134 +
1135 + /**
1136 + * Performs the 20.8 upgrade routine.
1137 + * Schedules another cleanup scheduled action.
1138 + *
1139 + * @return void
1140 + */
1141 + private function upgrade_208() {
1142 + if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1143 + wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1144 + }
1145 + }
1146 +
1147 + /**
1148 + * Performs the 22.6 upgrade routine.
1149 + * Schedules another cleanup scheduled action, but starting from the last cleanup action we just added (if there
1150 + * aren't any running cleanups already).
1151 + *
1152 + * @return void
1153 + */
1154 + private function upgrade_226() {
1155 + if ( get_option( Cleanup_Integration::CURRENT_TASK_OPTION ) === false ) {
1156 + $cleanup_integration = YoastSEO()->classes->get( Cleanup_Integration::class );
1157 + $cleanup_integration->start_cron_job( 'clean_selected_empty_usermeta', DAY_IN_SECONDS );
1158 + }
1159 + }
1160 +
1161 + /**
866 1162 * Sets the home_url option for the 15.1 upgrade routine.
867 1163 *
868 1164 * @return void
869 1165 */
@@ -889,8 +1185,10 @@
889 1185 /**
890 1186 * Checks if the indexable indexation is completed.
891 1187 * If so, sets the `indexables_indexation_completed` option to `true`,
892 1188 * else to `false`.
1189 + *
1190 + * @return void
893 1191 */
894 1192 public function set_indexation_completed_option_for_145() {
895 1193 WPSEO_Options::set( 'indexables_indexation_completed', YoastSEO()->helpers->indexing->get_limited_filtered_unindexed_count( 1 ) === 0 );
896 1194 }
@@ -896,8 +1194,10 @@
896 1194 }
897 1195
898 1196 /**
899 1197 * Cleans up the private taxonomies from the indexables table for the upgrade routine to 14.1.
1198 + *
1199 + * @return void
900 1200 */
901 1201 public function clean_up_private_taxonomies_for_141() {
902 1202 global $wpdb;
903 1203
@@ -905,27 +1205,35 @@
905 1205 $show_errors = $wpdb->show_errors;
906 1206 $wpdb->show_errors = false;
907 1207
908 1208 // Clean up indexables of private taxonomies.
909 - $private_taxonomies = \get_taxonomies( [ 'public' => false ], 'names' );
1209 + $private_taxonomies = get_taxonomies( [ 'public' => false ], 'names' );
910 1210
911 1211 if ( empty( $private_taxonomies ) ) {
912 1212 return;
913 1213 }
914 1214
915 - $indexable_table = Model::get_table_name( 'Indexable' );
1215 + $replacements = array_merge(
1216 + [
1217 + Model::get_table_name( 'Indexable' ),
1218 + 'object_type',
1219 + 'object_sub_type',
1220 + ],
1221 + $private_taxonomies,
1222 + );
916 1223
917 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
918 - $query = $wpdb->prepare(
919 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
920 - "DELETE FROM $indexable_table
921 - WHERE object_type = 'term'
922 - AND object_sub_type IN ("
923 - . \implode( ', ', \array_fill( 0, \count( $private_taxonomies ), '%s' ) )
1224 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1225 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1226 + $wpdb->query(
1227 + $wpdb->prepare(
1228 + "DELETE FROM %i
1229 + WHERE %i = 'term'
1230 + AND %i IN ("
1231 + . implode( ', ', array_fill( 0, count( $private_taxonomies ), '%s' ) )
924 1232 . ')',
925 - $private_taxonomies
1233 + $replacements,
1234 + ),
926 1235 );
927 - $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
928 1236
929 1237 $wpdb->show_errors = $show_errors;
930 1238 }
931 1239
@@ -930,8 +1238,10 @@
930 1238 }
931 1239
932 1240 /**
933 1241 * Resets the permalinks of attachments to `null` in the indexable table for the upgrade routine to 14.1.
1242 + *
1243 + * @return void
934 1244 */
935 1245 private function reset_permalinks_of_attachments_for_141() {
936 1246 global $wpdb;
937 1247
@@ -939,11 +1249,16 @@
939 1249 $show_errors = $wpdb->show_errors;
940 1250 $wpdb->show_errors = false;
941 1251
942 1252 // Reset the permalinks of the attachments in the indexable table.
943 - $indexable_table = Model::get_table_name( 'Indexable' );
944 - $query = "UPDATE $indexable_table SET permalink = NULL WHERE object_type = 'post' AND object_sub_type = 'attachment'";
945 - $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no user input.
1253 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1254 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1255 + $wpdb->query(
1256 + $wpdb->prepare(
1257 + "UPDATE %i SET %i = NULL WHERE %i = 'post' AND %i = 'attachment'",
1258 + [ Model::get_table_name( 'Indexable' ), 'permalink', 'object_type', 'object_sub_type' ],
1259 + ),
1260 + );
946 1261
947 1262 $wpdb->show_errors = $show_errors;
948 1263 }
949 1264
@@ -959,9 +1274,10 @@
959 1274 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-post-type-archive-notification' );
960 1275 }
961 1276
962 1277 /**
963 - * Removes the wpseo-suggested-plugin-yoast-acf-analysis notification from the Notification center for the 14.2 upgrade.
1278 + * Removes the wpseo-suggested-plugin-yoast-acf-analysis notification from the Notification center for the 14.2
1279 + * upgrade.
964 1280 *
965 1281 * @return void
966 1282 */
967 1283 public function remove_acf_notification_for_142() {
@@ -1013,9 +1329,16 @@
1013 1329 private function remove_sitemap_validators() {
1014 1330 global $wpdb;
1015 1331
1016 1332 // Remove all sitemap validators.
1017 - $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'wpseo_sitemap%validator%'" );
1333 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1334 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1335 + $wpdb->query(
1336 + $wpdb->prepare(
1337 + 'DELETE FROM %i WHERE %i LIKE %s',
1338 + [ $wpdb->options, 'option_name', 'wpseo_sitemap%validator%' ],
1339 + ),
1340 + );
1018 1341 }
1019 1342
1020 1343 /**
1021 1344 * Retrieves the option value directly from the database.
@@ -1021,16 +1344,25 @@
1021 1344 * Retrieves the option value directly from the database.
1022 1345 *
1023 1346 * @param string $option_name Option to retrieve.
1024 1347 *
1025 - * @return array|mixed The content of the option if exists, otherwise an empty array.
1348 + * @return int|string|bool|float|array<string|int|bool|float> The content of the option if exists, otherwise an
1349 + * empty array.
1026 1350 */
1027 1351 protected function get_option_from_database( $option_name ) {
1028 1352 global $wpdb;
1029 1353
1030 1354 // Load option directly from the database, to avoid filtering and sanitization.
1031 - $sql = $wpdb->prepare( 'SELECT option_value FROM ' . $wpdb->options . ' WHERE option_name = %s', $option_name );
1032 - $results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is is already prepared.
1355 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1356 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1357 + $results = $wpdb->get_results(
1358 + $wpdb->prepare(
1359 + 'SELECT %i FROM %i WHERE %i = %s',
1360 + [ 'option_value', $wpdb->options, 'option_name', $option_name ],
1361 + ),
1362 + ARRAY_A,
1363 + );
1364 +
1033 1365 if ( ! empty( $results ) ) {
1034 1366 return maybe_unserialize( $results[0]['option_value'] );
1035 1367 }
1036 1368
@@ -1061,18 +1393,17 @@
1061 1393
1062 1394 /**
1063 1395 * Saves an option setting to where it should be stored.
1064 1396 *
1065 - * @param array $source_data The option containing the value to be migrated.
1066 - * @param string $source_setting Name of the key in the "from" option.
1067 - * @param string|null $target_setting Name of the key in the "to" option.
1397 + * @param int|string|bool|float|array<string|int|bool|float> $source_data The option containing the value to be
1398 + * migrated.
1399 + * @param string $source_setting Name of the key in the "from" option.
1400 + * @param string|null $target_setting Name of the key in the "to" option.
1068 1401 *
1069 1402 * @return void
1070 1403 */
1071 1404 protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) {
1072 - if ( $target_setting === null ) {
1073 - $target_setting = $source_setting;
1074 - }
1405 + $target_setting ??= $source_setting;
1075 1406
1076 1407 if ( isset( $source_data[ $source_setting ] ) ) {
1077 1408 WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] );
1078 1409 }
@@ -1099,9 +1430,9 @@
1099 1430
1100 1431 WPSEO_Meta::set_value(
1101 1432 'title',
1102 1433 $option_title,
1103 - $shop_page_id
1434 + $shop_page_id,
1104 1435 );
1105 1436
1106 1437 WPSEO_Options::set( 'title-ptarchive-product', '' );
1107 1438 }
@@ -1113,9 +1444,9 @@
1113 1444
1114 1445 WPSEO_Meta::set_value(
1115 1446 'metadesc',
1116 1447 $option_metadesc,
1117 - $shop_page_id
1448 + $shop_page_id,
1118 1449 );
1119 1450
1120 1451 WPSEO_Options::set( 'metadesc-ptarchive-product', '' );
1121 1452 }
@@ -1127,9 +1458,9 @@
1127 1458
1128 1459 WPSEO_Meta::set_value(
1129 1460 'bctitle',
1130 1461 $option_bctitle,
1131 - $shop_page_id
1462 + $shop_page_id,
1132 1463 );
1133 1464
1134 1465 WPSEO_Options::set( 'bctitle-ptarchive-product', '' );
1135 1466 }
@@ -1141,9 +1472,9 @@
1141 1472
1142 1473 WPSEO_Meta::set_value(
1143 1474 'meta-robots-noindex',
1144 1475 $option_noindex,
1145 - $shop_page_id
1476 + $shop_page_id,
1146 1477 );
1147 1478
1148 1479 WPSEO_Options::set( 'noindex-ptarchive-product', false );
1149 1480 }
@@ -1253,6 +1584,288 @@
1253 1584
1254 1585 $wpseo_titles = array_merge( $wpseo_titles, $updated_options );
1255 1586
1256 1587 update_option( 'wpseo_titles', $wpseo_titles );
1588 + }
1589 +
1590 + /**
1591 + * Removes all indexables for posts that are not publicly viewable.
1592 + * This method should be called after init, because post_types can still be registered.
1593 + *
1594 + * @return void
1595 + */
1596 + public function remove_indexable_rows_for_non_public_post_types() {
1597 + global $wpdb;
1598 +
1599 + // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1600 + $show_errors = $wpdb->show_errors;
1601 + $wpdb->show_errors = false;
1602 +
1603 + $indexable_table = Model::get_table_name( 'Indexable' );
1604 +
1605 + $included_post_types = YoastSEO()->helpers->post_type->get_indexable_post_types();
1606 +
1607 + if ( empty( $included_post_types ) ) {
1608 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1609 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1610 + $wpdb->query(
1611 + $wpdb->prepare(
1612 + "DELETE FROM %i
1613 + WHERE %i = 'post'
1614 + AND %i IS NOT NULL",
1615 + [ $indexable_table, 'object_type', 'object_sub_type' ],
1616 + ),
1617 + );
1618 + }
1619 + else {
1620 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1621 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1622 + $wpdb->query(
1623 + $wpdb->prepare(
1624 + "DELETE FROM %i
1625 + WHERE %i = 'post'
1626 + AND %i IS NOT NULL
1627 + AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_post_types ), '%s' ) ) . ' )',
1628 + array_merge(
1629 + [
1630 + $indexable_table,
1631 + 'object_type',
1632 + 'object_sub_type',
1633 + 'object_sub_type',
1634 + ],
1635 + $included_post_types,
1636 + ),
1637 + ),
1638 + );
1639 + }
1640 +
1641 + $wpdb->show_errors = $show_errors;
1642 + }
1643 +
1644 + /**
1645 + * Removes all indexables for terms that are not publicly viewable.
1646 + * This method should be called after init, because taxonomies can still be registered.
1647 + *
1648 + * @return void
1649 + */
1650 + public function remove_indexable_rows_for_non_public_taxonomies() {
1651 + global $wpdb;
1652 +
1653 + // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1654 + $show_errors = $wpdb->show_errors;
1655 + $wpdb->show_errors = false;
1656 +
1657 + $indexable_table = Model::get_table_name( 'Indexable' );
1658 +
1659 + $included_taxonomies = YoastSEO()->helpers->taxonomy->get_indexable_taxonomies();
1660 +
1661 + if ( empty( $included_taxonomies ) ) {
1662 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1663 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1664 + $wpdb->query(
1665 + $wpdb->prepare(
1666 + "DELETE FROM %i
1667 + WHERE %i = 'term'
1668 + AND %i IS NOT NULL",
1669 + [ $indexable_table, 'object_type', 'object_sub_type' ],
1670 + ),
1671 + );
1672 + }
1673 + else {
1674 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1675 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1676 + $wpdb->query(
1677 + $wpdb->prepare(
1678 + "DELETE FROM %i
1679 + WHERE %i = 'term'
1680 + AND %i IS NOT NULL
1681 + AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_taxonomies ), '%s' ) ) . ' )',
1682 + array_merge(
1683 + [
1684 + $indexable_table,
1685 + 'object_type',
1686 + 'object_sub_type',
1687 + 'object_sub_type',
1688 + ],
1689 + $included_taxonomies,
1690 + ),
1691 + ),
1692 + );
1693 + }
1694 +
1695 + $wpdb->show_errors = $show_errors;
1696 + }
1697 +
1698 + /**
1699 + * De-duplicates indexables that have more than one "unindexed" rows for the same object. Keeps the newest
1700 + * indexable.
1701 + *
1702 + * @return void
1703 + */
1704 + protected function deduplicate_unindexed_indexable_rows() {
1705 + global $wpdb;
1706 +
1707 + // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1708 + $show_errors = $wpdb->show_errors;
1709 + $wpdb->show_errors = false;
1710 +
1711 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1712 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1713 + $duplicates = $wpdb->get_results(
1714 + $wpdb->prepare(
1715 + "
1716 + SELECT
1717 + MAX(id) as newest_id,
1718 + object_id,
1719 + object_type
1720 + FROM
1721 + %i
1722 + WHERE
1723 + post_status = 'unindexed'
1724 + AND object_type IN ( 'term', 'post', 'user' )
1725 + GROUP BY
1726 + object_id,
1727 + object_type
1728 + HAVING
1729 + count(*) > 1",
1730 + [ Model::get_table_name( 'Indexable' ) ],
1731 + ),
1732 + ARRAY_A,
1733 + );
1734 +
1735 + if ( empty( $duplicates ) ) {
1736 + $wpdb->show_errors = $show_errors;
1737 +
1738 + return;
1739 + }
1740 +
1741 + // Users, terms and posts may share the same object_id. So delete them in separate, more performant, queries.
1742 + $delete_queries = [
1743 + $this->get_indexable_deduplication_query_for_type( 'post', $duplicates, $wpdb ),
1744 + $this->get_indexable_deduplication_query_for_type( 'term', $duplicates, $wpdb ),
1745 + $this->get_indexable_deduplication_query_for_type( 'user', $duplicates, $wpdb ),
1746 + ];
1747 +
1748 + foreach ( $delete_queries as $delete_query ) {
1749 + if ( ! empty( $delete_query ) ) {
1750 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1751 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1752 + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
1753 + $wpdb->query( $delete_query );
1754 + // phpcs:enable
1755 + }
1756 + }
1757 +
1758 + $wpdb->show_errors = $show_errors;
1759 + }
1760 +
1761 + /**
1762 + * Cleans up "unindexed" indexable rows when appropriate, aka when there's no object ID even though it should.
1763 + *
1764 + * @return void
1765 + */
1766 + protected function clean_unindexed_indexable_rows_with_no_object_id() {
1767 + global $wpdb;
1768 +
1769 + // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1770 + $show_errors = $wpdb->show_errors;
1771 + $wpdb->show_errors = false;
1772 +
1773 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1774 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1775 + $wpdb->query(
1776 + $wpdb->prepare(
1777 + "DELETE FROM %i
1778 + WHERE %i = 'unindexed'
1779 + AND %i NOT IN ( 'home-page', 'date-archive', 'post-type-archive', 'system-page' )
1780 + AND %i IS NULL",
1781 + [ Model::get_table_name( 'Indexable' ), 'post_status', 'object_type', 'object_id' ],
1782 + ),
1783 + );
1784 +
1785 + $wpdb->show_errors = $show_errors;
1786 + }
1787 +
1788 + /**
1789 + * Removes all user indexable rows when the author archive is disabled.
1790 + *
1791 + * @return void
1792 + */
1793 + protected function remove_indexable_rows_for_disabled_authors_archive() {
1794 + global $wpdb;
1795 +
1796 + if ( ! YoastSEO()->helpers->author_archive->are_disabled() ) {
1797 + return;
1798 + }
1799 +
1800 + // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1801 + $show_errors = $wpdb->show_errors;
1802 + $wpdb->show_errors = false;
1803 +
1804 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1805 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1806 + $wpdb->query(
1807 + $wpdb->prepare(
1808 + "DELETE FROM %i WHERE %i = 'user'",
1809 + [ Model::get_table_name( 'Indexable' ), 'object_type' ],
1810 + ),
1811 + );
1812 +
1813 + $wpdb->show_errors = $show_errors;
1814 + }
1815 +
1816 + /**
1817 + * Creates a query for de-duplicating indexables for a particular type.
1818 + *
1819 + * @param string $object_type The object type to deduplicate.
1820 + * @param string|array<array<int,int,string>> $duplicates The result of the duplicate query.
1821 + * @param wpdb $wpdb The wpdb object.
1822 + *
1823 + * @return string The query that removes all but one duplicate for each object of the object type.
1824 + */
1825 + protected function get_indexable_deduplication_query_for_type( $object_type, $duplicates, $wpdb ) {
1826 + $filtered_duplicates = array_filter(
1827 + $duplicates,
1828 + static function ( $duplicate ) use ( $object_type ) {
1829 + return $duplicate['object_type'] === $object_type;
1830 + },
1831 + );
1832 +
1833 + if ( empty( $filtered_duplicates ) ) {
1834 + return '';
1835 + }
1836 +
1837 + $object_ids = wp_list_pluck( $filtered_duplicates, 'object_id' );
1838 + $newest_indexable_ids = wp_list_pluck( $filtered_duplicates, 'newest_id' );
1839 +
1840 + $replacements = array_merge(
1841 + [
1842 + Model::get_table_name( 'Indexable' ),
1843 + 'object_id',
1844 + ],
1845 + array_values( $object_ids ),
1846 + array_values( $newest_indexable_ids ),
1847 + );
1848 + $replacements[] = $object_type;
1849 +
1850 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
1851 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
1852 + return $wpdb->prepare(
1853 + 'DELETE FROM
1854 + %i
1855 + WHERE
1856 + %i IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' )
1857 + AND id NOT IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' )
1858 + AND object_type = %s',
1859 + $replacements,
1860 + );
1861 + }
1862 +
1863 + /**
1864 + * Removes the settings' introduction modal data for users.
1865 + *
1866 + * @return void
1867 + */
1868 + public function delete_user_introduction_meta() {
1869 + delete_metadata( 'user', 0, '_yoast_settings_introduction', '', true );
1257 1870 }
1258 1871 }