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