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 +672 -67 18.428.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 /**
@@ -76,12 +79,22 @@
76 79 '17.2-RC0' => 'upgrade_172',
77 80 '17.7.1-RC0' => 'upgrade_1771',
78 81 '17.9-RC0' => 'upgrade_179',
79 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',
80 94 ];
81 95
82 96 array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version );
83 -
84 97 if ( version_compare( $version, '12.5-RC0', '<' ) ) {
85 98 /*
86 99 * We have to run this by hook, because otherwise:
87 100 * - the theme support check isn't available.
@@ -89,16 +102,12 @@
89 102 */
90 103 add_action( 'init', [ $this, 'upgrade_125' ] );
91 104 }
92 105
93 - // Since 3.7.
94 - $upsell_notice = new WPSEO_Product_Upsell_Notice();
95 - $upsell_notice->set_upgrade_notice();
96 -
97 106 /**
98 107 * Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO.
99 108 *
100 - * @api string - The current version of Yoast SEO
109 + * @param string $version The current version of Yoast SEO
101 110 */
102 111 do_action( 'wpseo_run_upgrade', $version );
103 112
104 113 $this->finish_up( $version );
@@ -123,8 +132,10 @@
123 132 * Adds a new upgrade history entry.
124 133 *
125 134 * @param string $current_version The old version from which we are upgrading.
126 135 * @param string $new_version The version we are upgrading to.
136 + *
137 + * @return void
127 138 */
128 139 protected function add_upgrade_history( $current_version, $new_version ) {
129 140 $upgrade_history = new WPSEO_Upgrade_History();
130 141 $upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) );
@@ -138,11 +149,13 @@
138 149 * @return void
139 150 */
140 151 protected function finish_up( $previous_version = null ) {
141 152 if ( $previous_version ) {
142 - 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' );
143 156 }
144 - WPSEO_Options::set( 'version', WPSEO_VERSION );
157 + WPSEO_Options::set( 'version', WPSEO_VERSION, 'wpseo' );
145 158
146 159 // Just flush rewrites, always, to at least make them work after an upgrade.
147 160 add_action( 'shutdown', 'flush_rewrite_rules' );
148 161
@@ -156,8 +169,10 @@
156 169 /**
157 170 * Run the Yoast SEO 1.5 upgrade routine.
158 171 *
159 172 * @param string $version Current plugin version.
173 + *
174 + * @return void
160 175 */
161 176 private function upgrade_15( $version ) {
162 177 // Clean up options and meta.
163 178 WPSEO_Options::clean_up( null, $version );
@@ -165,8 +180,10 @@
165 180 }
166 181
167 182 /**
168 183 * Moves options that moved position in WPSEO 2.0.
184 + *
185 + * @return void
169 186 */
170 187 private function upgrade_20() {
171 188 /**
172 189 * Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table.
@@ -182,8 +199,10 @@
182 199 }
183 200
184 201 /**
185 202 * Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly.
203 + *
204 + * @return void
186 205 */
187 206 private function upgrade_21() {
188 207 $taxonomies = get_option( 'wpseo_taxonomy_meta', [] );
189 208
@@ -205,8 +224,10 @@
205 224 }
206 225
207 226 /**
208 227 * Performs upgrade functions to Yoast SEO 2.2.
228 + *
229 + * @return void
209 230 */
210 231 private function upgrade_22() {
211 232 // Unschedule our tracking.
212 233 wp_clear_scheduled_hook( 'yoast_tracking' );
@@ -215,8 +236,10 @@
215 236 }
216 237
217 238 /**
218 239 * Schedules upgrade function to Yoast SEO 2.3.
240 + *
241 + * @return void
219 242 */
220 243 private function upgrade_23() {
221 244 add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 );
222 245 add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 );
@@ -223,10 +246,14 @@
223 246 }
224 247
225 248 /**
226 249 * Performs upgrade query to Yoast SEO 2.3.
250 + *
251 + * @return void
227 252 */
228 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.
229 256 $wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' );
230 257
231 258 if ( ! empty( $wp_query->posts ) ) {
232 259 $options = get_option( 'wpseo_xml' );
@@ -254,8 +281,10 @@
254 281 }
255 282
256 283 /**
257 284 * Performs upgrade functions to Yoast SEO 3.0.
285 + *
286 + * @return void
258 287 */
259 288 private function upgrade_30() {
260 289 // Remove the meta fields for sitemap prio.
261 290 delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' );
@@ -262,8 +291,10 @@
262 291 }
263 292
264 293 /**
265 294 * Performs upgrade functions to Yoast SEO 3.3.
295 + *
296 + * @return void
266 297 */
267 298 private function upgrade_33() {
268 299 // Notification dismissals have been moved to User Meta instead of global option.
269 300 delete_option( Yoast_Notification_Center::STORAGE_KEY );
@@ -270,18 +301,29 @@
270 301 }
271 302
272 303 /**
273 304 * Performs upgrade functions to Yoast SEO 3.6.
305 + *
306 + * @return void
274 307 */
275 - private function upgrade_36() {
308 + protected function upgrade_36() {
276 309 global $wpdb;
277 310
278 311 // Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled.
279 - $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 + );
280 320 }
281 321
282 322 /**
283 323 * Removes the about notice when its still in the database.
324 + *
325 + * @return void
284 326 */
285 327 private function upgrade_40() {
286 328 $center = Yoast_Notification_Center::get();
287 329 $center->remove_notification_by_id( 'wpseo-dismiss-about' );
@@ -288,8 +330,10 @@
288 330 }
289 331
290 332 /**
291 333 * Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo.
334 + *
335 + * @return void
292 336 */
293 337 private function upgrade_44() {
294 338 $wpseo_titles = $this->get_option_from_database( 'wpseo_titles' );
295 339
@@ -301,25 +345,31 @@
301 345 }
302 346
303 347 /**
304 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
305 351 */
306 352 private function upgrade_47() {
307 353 global $wpdb;
308 354
309 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.
310 358 $wpdb->query(
311 359 $wpdb->prepare(
312 360 'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"',
313 - WPSEO_Cornerstone_Filter::META_NAME
314 - )
361 + WPSEO_Cornerstone_Filter::META_NAME,
362 + ),
315 363 );
316 364 }
317 365
318 366 /**
319 367 * Removes the 'wpseo-dismiss-about' notice for every user that still has it.
368 + *
369 + * @return void
320 370 */
321 - private function upgrade_49() {
371 + protected function upgrade_49() {
322 372 global $wpdb;
323 373
324 374 /*
325 375 * Using a filter to remove the notification for the current logged in user. The notification center is
@@ -330,19 +380,28 @@
330 380 add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] );
331 381
332 382 $meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY;
333 383
384 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
385 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
334 386 $usermetas = $wpdb->get_results(
335 387 $wpdb->prepare(
336 388 '
337 - SELECT user_id, meta_value
338 - FROM ' . $wpdb->usermeta . '
339 - WHERE meta_key = %s AND meta_value LIKE %s
389 + SELECT %i, %i
390 + FROM %i
391 + WHERE %i = %s AND %i LIKE %s
340 392 ',
341 - $meta_key,
342 - '%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 + ],
343 402 ),
344 - ARRAY_A
403 + ARRAY_A,
345 404 );
346 405
347 406 if ( empty( $usermetas ) ) {
348 407 return;
@@ -379,18 +438,30 @@
379 438 }
380 439
381 440 /**
382 441 * Adds the yoast_seo_links table to the database.
442 + *
443 + * @return void
383 444 */
384 - private function upgrade_50() {
445 + protected function upgrade_50() {
385 446 global $wpdb;
386 447
387 448 // Deletes the post meta value, which might created in the RC.
388 - $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 + );
389 458 }
390 459
391 460 /**
392 461 * Register new capabilities and roles.
462 + *
463 + * @return void
393 464 */
394 465 private function upgrade_55() {
395 466 // Register roles.
396 467 do_action( 'wpseo_register_roles' );
@@ -429,9 +500,8 @@
429 500
430 501 // Move one XML sitemap setting, then delete the option.
431 502 $this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' );
432 503
433 -
434 504 // Move the RSS settings to the search appearance settings, then delete the RSS option.
435 505 $this->save_option_setting( $wpseo_rss, 'rssbefore' );
436 506 $this->save_option_setting( $wpseo_rss, 'rssafter' );
437 507
@@ -476,8 +546,10 @@
476 546 $yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' );
477 547
478 548 // Moves the user meta for excluding from the XML sitemap to a noindex.
479 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.
480 552 $wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" );
481 553 }
482 554
483 555 /**
@@ -505,8 +577,10 @@
505 577 */
506 578 private function upgrade_73() {
507 579 global $wpdb;
508 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.
509 583 $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" );
510 584
511 585 // Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2.
512 586 delete_option( 'whip_dismiss_timestamp' );
@@ -516,9 +590,9 @@
516 590 * Performs the 7.4 upgrade.
517 591 *
518 592 * @return void
519 593 */
520 - private function upgrade_74() {
594 + protected function upgrade_74() {
521 595 $this->remove_sitemap_validators();
522 596 }
523 597
524 598 /**
@@ -568,9 +642,9 @@
568 642 * Performs the 9.0 upgrade.
569 643 *
570 644 * @return void
571 645 */
572 - private function upgrade_90() {
646 + protected function upgrade_90() {
573 647 global $wpdb;
574 648
575 649 // Invalidate all sitemap cache transients.
576 650 WPSEO_Sitemaps_Cache_Validator::cleanup_database();
@@ -577,9 +651,17 @@
577 651
578 652 // Removes all scheduled tasks for hitting the sitemap index.
579 653 wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' );
580 654
581 - $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 + );
582 664 }
583 665
584 666 /**
585 667 * Performs the 10.0 upgrade.
@@ -612,8 +694,10 @@
612 694 /**
613 695 * Performs the 12.3 upgrade.
614 696 *
615 697 * Removes the about notice when its still in the database.
698 + *
699 + * @return void
616 700 */
617 701 private function upgrade_123() {
618 702 $plugins = [
619 703 'yoast-seo-premium',
@@ -633,8 +717,10 @@
633 717 /**
634 718 * Performs the 12.4 upgrade.
635 719 *
636 720 * Removes the Google plus defaults from the database.
721 + *
722 + * @return void
637 723 */
638 724 private function upgrade_124() {
639 725 $this->cleanup_option_data( 'wpseo_social' );
640 726 }
@@ -640,8 +726,10 @@
640 726 }
641 727
642 728 /**
643 729 * Performs the 12.5 upgrade.
730 + *
731 + * @return void
644 732 */
645 733 public function upgrade_125() {
646 734 // Disables the force rewrite title when the theme supports it through WordPress.
647 735 if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) {
@@ -648,9 +736,17 @@
648 736 WPSEO_Options::set( 'forcerewritetitle', false );
649 737 }
650 738
651 739 global $wpdb;
652 - $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 + );
653 749
654 750 // Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released.
655 751 $center = Yoast_Notification_Center::get();
656 752 $center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
@@ -657,8 +753,10 @@
657 753 }
658 754
659 755 /**
660 756 * Performs the 12.8 upgrade.
757 + *
758 + * @return void
661 759 */
662 760 private function upgrade_128() {
663 761 // Re-save wpseo to make sure bf_banner_2019_dismissed key is gone.
664 762 $this->cleanup_option_data( 'wpseo' );
@@ -668,8 +766,10 @@
668 766 }
669 767
670 768 /**
671 769 * Performs the 13.2 upgrade.
770 + *
771 + * @return void
672 772 */
673 773 private function upgrade_132() {
674 774 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-tagline-notice' );
675 775 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-permalink-notice' );
@@ -704,8 +804,10 @@
704 804 }
705 805
706 806 /**
707 807 * Perform the 14.0.3 upgrade.
808 + *
809 + * @return void
708 810 */
709 811 private function upgrade_1403() {
710 812 WPSEO_Options::set( 'ignore_indexation_warning', false );
711 813 }
@@ -711,8 +813,10 @@
711 813 }
712 814
713 815 /**
714 816 * Performs the 14.1 upgrade.
817 + *
818 + * @return void
715 819 */
716 820 private function upgrade_141() {
717 821 /*
718 822 * These notifications are retrieved from storage on the `init` hook with
@@ -727,8 +831,10 @@
727 831 /**
728 832 * Performs the 14.2 upgrade.
729 833 *
730 834 * Removes the yoast-acf-analysis notice when it's still in the database.
835 + *
836 + * @return void
731 837 */
732 838 private function upgrade_142() {
733 839 add_action( 'init', [ $this, 'remove_acf_notification_for_142' ] );
734 840 }
@@ -734,8 +840,10 @@
734 840 }
735 841
736 842 /**
737 843 * Performs the 14.5 upgrade.
844 + *
845 + * @return void
738 846 */
739 847 private function upgrade_145() {
740 848 add_action( 'init', [ $this, 'set_indexation_completed_option_for_145' ] );
741 849 }
@@ -741,8 +849,10 @@
741 849 }
742 850
743 851 /**
744 852 * Performs the 14.9 upgrade.
853 + *
854 + * @return void
745 855 */
746 856 private function upgrade_149() {
747 857 $version = get_option( 'wpseo_license_server_version', 2 );
748 858 WPSEO_Options::set( 'license_server_version', $version );
@@ -804,21 +914,25 @@
804 914 }
805 915
806 916 /**
807 917 * Performs the 15.9.1 upgrade routine.
918 + *
919 + * @return void
808 920 */
809 921 private function upgrade_1591() {
810 - $enabled_auto_updates = \get_option( 'auto_update_plugins' );
811 - $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 );
812 924 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', [], $enabled_auto_updates );
813 925 }
814 926
815 927 /**
816 928 * Performs the 16.2 upgrade routine.
929 + *
930 + * @return void
817 931 */
818 932 private function upgrade_162() {
819 - $enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
820 - $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 );
821 935 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
822 936 }
823 937
824 938 /**
@@ -833,32 +947,37 @@
833 947 add_action( 'init', [ $this, 'reset_og_settings_to_default_values' ], 100 );
834 948 }
835 949
836 950 /**
837 - * 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.
838 953 *
839 954 * @return void
840 955 */
841 956 private function upgrade_172() {
842 - \wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' );
843 - \wp_unschedule_hook( 'wpseo_cleanup_indexables' );
957 + wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' );
958 + wp_unschedule_hook( 'wpseo_cleanup_indexables' );
844 959
845 - if ( ! \wp_next_scheduled( \Yoast\WP\SEO\Integrations\Cleanup_Integration::START_HOOK ) ) {
846 - \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 );
847 962 }
848 963 }
849 964
850 965 /**
851 966 * Performs the 17.7.1 upgrade routine.
967 + *
968 + * @return void
852 969 */
853 970 private function upgrade_1771() {
854 - $enabled_auto_updates = \get_site_option( 'auto_update_plugins' );
855 - $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 );
856 973 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
857 974 }
858 975
859 976 /**
860 977 * Performs the 17.9 upgrade routine.
978 + *
979 + * @return void
861 980 */
862 981 private function upgrade_179() {
863 982 WPSEO_Options::set( 'wincher_integration_active', true );
864 983 }
@@ -864,8 +983,10 @@
864 983 }
865 984
866 985 /**
867 986 * Performs the 18.3 upgrade routine.
987 + *
988 + * @return void
868 989 */
869 990 private function upgrade_183() {
870 991 $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' );
871 992 }
@@ -870,8 +991,175 @@
870 991 $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' );
871 992 }
872 993
873 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 + /**
874 1162 * Sets the home_url option for the 15.1 upgrade routine.
875 1163 *
876 1164 * @return void
877 1165 */
@@ -897,8 +1185,10 @@
897 1185 /**
898 1186 * Checks if the indexable indexation is completed.
899 1187 * If so, sets the `indexables_indexation_completed` option to `true`,
900 1188 * else to `false`.
1189 + *
1190 + * @return void
901 1191 */
902 1192 public function set_indexation_completed_option_for_145() {
903 1193 WPSEO_Options::set( 'indexables_indexation_completed', YoastSEO()->helpers->indexing->get_limited_filtered_unindexed_count( 1 ) === 0 );
904 1194 }
@@ -904,8 +1194,10 @@
904 1194 }
905 1195
906 1196 /**
907 1197 * Cleans up the private taxonomies from the indexables table for the upgrade routine to 14.1.
1198 + *
1199 + * @return void
908 1200 */
909 1201 public function clean_up_private_taxonomies_for_141() {
910 1202 global $wpdb;
911 1203
@@ -913,27 +1205,35 @@
913 1205 $show_errors = $wpdb->show_errors;
914 1206 $wpdb->show_errors = false;
915 1207
916 1208 // Clean up indexables of private taxonomies.
917 - $private_taxonomies = \get_taxonomies( [ 'public' => false ], 'names' );
1209 + $private_taxonomies = get_taxonomies( [ 'public' => false ], 'names' );
918 1210
919 1211 if ( empty( $private_taxonomies ) ) {
920 1212 return;
921 1213 }
922 1214
923 - $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 + );
924 1223
925 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
926 - $query = $wpdb->prepare(
927 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix.
928 - "DELETE FROM $indexable_table
929 - WHERE object_type = 'term'
930 - AND object_sub_type IN ("
931 - . \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' ) )
932 1232 . ')',
933 - $private_taxonomies
1233 + $replacements,
1234 + ),
934 1235 );
935 - $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
936 1236
937 1237 $wpdb->show_errors = $show_errors;
938 1238 }
939 1239
@@ -938,8 +1238,10 @@
938 1238 }
939 1239
940 1240 /**
941 1241 * Resets the permalinks of attachments to `null` in the indexable table for the upgrade routine to 14.1.
1242 + *
1243 + * @return void
942 1244 */
943 1245 private function reset_permalinks_of_attachments_for_141() {
944 1246 global $wpdb;
945 1247
@@ -947,11 +1249,16 @@
947 1249 $show_errors = $wpdb->show_errors;
948 1250 $wpdb->show_errors = false;
949 1251
950 1252 // Reset the permalinks of the attachments in the indexable table.
951 - $indexable_table = Model::get_table_name( 'Indexable' );
952 - $query = "UPDATE $indexable_table SET permalink = NULL WHERE object_type = 'post' AND object_sub_type = 'attachment'";
953 - $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 + );
954 1261
955 1262 $wpdb->show_errors = $show_errors;
956 1263 }
957 1264
@@ -967,9 +1274,10 @@
967 1274 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-post-type-archive-notification' );
968 1275 }
969 1276
970 1277 /**
971 - * 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.
972 1280 *
973 1281 * @return void
974 1282 */
975 1283 public function remove_acf_notification_for_142() {
@@ -1021,9 +1329,16 @@
1021 1329 private function remove_sitemap_validators() {
1022 1330 global $wpdb;
1023 1331
1024 1332 // Remove all sitemap validators.
1025 - $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 + );
1026 1341 }
1027 1342
1028 1343 /**
1029 1344 * Retrieves the option value directly from the database.
@@ -1029,16 +1344,25 @@
1029 1344 * Retrieves the option value directly from the database.
1030 1345 *
1031 1346 * @param string $option_name Option to retrieve.
1032 1347 *
1033 - * @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.
1034 1350 */
1035 1351 protected function get_option_from_database( $option_name ) {
1036 1352 global $wpdb;
1037 1353
1038 1354 // Load option directly from the database, to avoid filtering and sanitization.
1039 - $sql = $wpdb->prepare( 'SELECT option_value FROM ' . $wpdb->options . ' WHERE option_name = %s', $option_name );
1040 - $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 +
1041 1365 if ( ! empty( $results ) ) {
1042 1366 return maybe_unserialize( $results[0]['option_value'] );
1043 1367 }
1044 1368
@@ -1069,18 +1393,17 @@
1069 1393
1070 1394 /**
1071 1395 * Saves an option setting to where it should be stored.
1072 1396 *
1073 - * @param array $source_data The option containing the value to be migrated.
1074 - * @param string $source_setting Name of the key in the "from" option.
1075 - * @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.
1076 1401 *
1077 1402 * @return void
1078 1403 */
1079 1404 protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) {
1080 - if ( $target_setting === null ) {
1081 - $target_setting = $source_setting;
1082 - }
1405 + $target_setting ??= $source_setting;
1083 1406
1084 1407 if ( isset( $source_data[ $source_setting ] ) ) {
1085 1408 WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] );
1086 1409 }
@@ -1107,9 +1430,9 @@
1107 1430
1108 1431 WPSEO_Meta::set_value(
1109 1432 'title',
1110 1433 $option_title,
1111 - $shop_page_id
1434 + $shop_page_id,
1112 1435 );
1113 1436
1114 1437 WPSEO_Options::set( 'title-ptarchive-product', '' );
1115 1438 }
@@ -1121,9 +1444,9 @@
1121 1444
1122 1445 WPSEO_Meta::set_value(
1123 1446 'metadesc',
1124 1447 $option_metadesc,
1125 - $shop_page_id
1448 + $shop_page_id,
1126 1449 );
1127 1450
1128 1451 WPSEO_Options::set( 'metadesc-ptarchive-product', '' );
1129 1452 }
@@ -1135,9 +1458,9 @@
1135 1458
1136 1459 WPSEO_Meta::set_value(
1137 1460 'bctitle',
1138 1461 $option_bctitle,
1139 - $shop_page_id
1462 + $shop_page_id,
1140 1463 );
1141 1464
1142 1465 WPSEO_Options::set( 'bctitle-ptarchive-product', '' );
1143 1466 }
@@ -1149,9 +1472,9 @@
1149 1472
1150 1473 WPSEO_Meta::set_value(
1151 1474 'meta-robots-noindex',
1152 1475 $option_noindex,
1153 - $shop_page_id
1476 + $shop_page_id,
1154 1477 );
1155 1478
1156 1479 WPSEO_Options::set( 'noindex-ptarchive-product', false );
1157 1480 }
@@ -1261,6 +1584,288 @@
1261 1584
1262 1585 $wpseo_titles = array_merge( $wpseo_titles, $updated_options );
1263 1586
1264 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 );
1265 1870 }
1266 1871 }