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