PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / inc / class-upgrade.php

class-upgrade.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at inc/class-upgrade.php

1,872 lines 55.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Internal
6 */
7
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;
12
13 /**
14 * This code handles the option upgrades.
15 */
16 class WPSEO_Upgrade {
17
18 /**
19 * The taxonomy helper.
20 *
21 * @var Taxonomy_Helper
22 */
23 private $taxonomy_helper;
24
25 /**
26 * Class constructor.
27 */
28 public function __construct() {
29 $this->taxonomy_helper = YoastSEO()->helpers->taxonomy;
30
31 $version = WPSEO_Options::get( 'version' );
32
33 WPSEO_Options::maybe_set_multisite_defaults( false );
34
35 $routines = [
36 '1.5.0' => 'upgrade_15',
37 '2.0' => 'upgrade_20',
38 '2.1' => 'upgrade_21',
39 '2.2' => 'upgrade_22',
40 '2.3' => 'upgrade_23',
41 '3.0' => 'upgrade_30',
42 '3.3' => 'upgrade_33',
43 '3.6' => 'upgrade_36',
44 '4.0' => 'upgrade_40',
45 '4.4' => 'upgrade_44',
46 '4.7' => 'upgrade_47',
47 '4.9' => 'upgrade_49',
48 '5.0' => 'upgrade_50',
49 '5.5' => 'upgrade_55',
50 '6.3' => 'upgrade_63',
51 '7.0-RC0' => 'upgrade_70',
52 '7.1-RC0' => 'upgrade_71',
53 '7.3-RC0' => 'upgrade_73',
54 '7.4-RC0' => 'upgrade_74',
55 '7.5.3' => 'upgrade_753',
56 '7.7-RC0' => 'upgrade_77',
57 '7.7.2-RC0' => 'upgrade_772',
58 '9.0-RC0' => 'upgrade_90',
59 '10.0-RC0' => 'upgrade_100',
60 '11.1-RC0' => 'upgrade_111',
61 // Reset notifications because we removed the AMP Glue plugin notification.
62 '12.1-RC0' => 'clean_all_notifications',
63 '12.3-RC0' => 'upgrade_123',
64 '12.4-RC0' => 'upgrade_124',
65 '12.8-RC0' => 'upgrade_128',
66 '13.2-RC0' => 'upgrade_132',
67 '14.0.3-RC0' => 'upgrade_1403',
68 '14.1-RC0' => 'upgrade_141',
69 '14.2-RC0' => 'upgrade_142',
70 '14.5-RC0' => 'upgrade_145',
71 '14.9-RC0' => 'upgrade_149',
72 '15.1-RC0' => 'upgrade_151',
73 '15.3-RC0' => 'upgrade_153',
74 '15.5-RC0' => 'upgrade_155',
75 '15.7-RC0' => 'upgrade_157',
76 '15.9.1-RC0' => 'upgrade_1591',
77 '16.2-RC0' => 'upgrade_162',
78 '16.5-RC0' => 'upgrade_165',
79 '17.2-RC0' => 'upgrade_172',
80 '17.7.1-RC0' => 'upgrade_1771',
81 '17.9-RC0' => 'upgrade_179',
82 '18.3-RC3' => 'upgrade_183',
83 '18.6-RC0' => 'upgrade_186',
84 '18.9-RC0' => 'upgrade_189',
85 '19.1-RC0' => 'upgrade_191',
86 '19.3-RC0' => 'upgrade_193',
87 '19.6-RC0' => 'upgrade_196',
88 '19.11-RC0' => 'upgrade_1911',
89 '20.2-RC0' => 'upgrade_202',
90 '20.5-RC0' => 'upgrade_205',
91 '20.7-RC0' => 'upgrade_207',
92 '20.8-RC0' => 'upgrade_208',
93 '22.6-RC0' => 'upgrade_226',
94 ];
95
96 array_walk( $routines, [ $this, 'run_upgrade_routine' ], $version );
97 if ( version_compare( $version, '12.5-RC0', '<' ) ) {
98 /*
99 * We have to run this by hook, because otherwise:
100 * - the theme support check isn't available.
101 * - the notification center notifications are not filled yet.
102 */
103 add_action( 'init', [ $this, 'upgrade_125' ] );
104 }
105
106 /**
107 * Filter: 'wpseo_run_upgrade' - Runs the upgrade hook which are dependent on Yoast SEO.
108 *
109 * @param string $version The current version of Yoast SEO
110 */
111 do_action( 'wpseo_run_upgrade', $version );
112
113 $this->finish_up( $version );
114 }
115
116 /**
117 * Runs the upgrade routine.
118 *
119 * @param string $routine The method to call.
120 * @param string $version The new version.
121 * @param string $current_version The current set version.
122 *
123 * @return void
124 */
125 protected function run_upgrade_routine( $routine, $version, $current_version ) {
126 if ( version_compare( $current_version, $version, '<' ) ) {
127 $this->$routine( $current_version );
128 }
129 }
130
131 /**
132 * Adds a new upgrade history entry.
133 *
134 * @param string $current_version The old version from which we are upgrading.
135 * @param string $new_version The version we are upgrading to.
136 *
137 * @return void
138 */
139 protected function add_upgrade_history( $current_version, $new_version ) {
140 $upgrade_history = new WPSEO_Upgrade_History();
141 $upgrade_history->add( $current_version, $new_version, array_keys( WPSEO_Options::$options ) );
142 }
143
144 /**
145 * Runs the needed cleanup after an update, setting the DB version to latest version, flushing caches etc.
146 *
147 * @param string|null $previous_version The previous version.
148 *
149 * @return void
150 */
151 protected function finish_up( $previous_version = null ) {
152 if ( $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' );
156 }
157 WPSEO_Options::set( 'version', WPSEO_VERSION, 'wpseo' );
158
159 // Just flush rewrites, always, to at least make them work after an upgrade.
160 add_action( 'shutdown', 'flush_rewrite_rules' );
161
162 // Flush the sitemap cache.
163 WPSEO_Sitemaps_Cache::clear();
164
165 // Make sure all our options always exist - issue #1245.
166 WPSEO_Options::ensure_options_exist();
167 }
168
169 /**
170 * Run the Yoast SEO 1.5 upgrade routine.
171 *
172 * @param string $version Current plugin version.
173 *
174 * @return void
175 */
176 private function upgrade_15( $version ) {
177 // Clean up options and meta.
178 WPSEO_Options::clean_up( null, $version );
179 WPSEO_Meta::clean_up();
180 }
181
182 /**
183 * Moves options that moved position in WPSEO 2.0.
184 *
185 * @return void
186 */
187 private function upgrade_20() {
188 /**
189 * Clean up stray wpseo_ms options from the options table, option should only exist in the sitemeta table.
190 * This could have been caused in many version of Yoast SEO, so deleting it for everything below 2.0.
191 */
192 delete_option( 'wpseo_ms' );
193
194 $wpseo = $this->get_option_from_database( 'wpseo' );
195 $this->save_option_setting( $wpseo, 'pinterestverify' );
196
197 // Re-save option to trigger sanitization.
198 $this->cleanup_option_data( 'wpseo' );
199 }
200
201 /**
202 * Detects if taxonomy terms were split and updates the corresponding taxonomy meta's accordingly.
203 *
204 * @return void
205 */
206 private function upgrade_21() {
207 $taxonomies = get_option( 'wpseo_taxonomy_meta', [] );
208
209 if ( ! empty( $taxonomies ) ) {
210 foreach ( $taxonomies as $taxonomy => $tax_metas ) {
211 foreach ( $tax_metas as $term_id => $tax_meta ) {
212 if ( function_exists( 'wp_get_split_term' ) ) {
213 $new_term_id = wp_get_split_term( $term_id, $taxonomy );
214 if ( $new_term_id !== false ) {
215 $taxonomies[ $taxonomy ][ $new_term_id ] = $taxonomies[ $taxonomy ][ $term_id ];
216 unset( $taxonomies[ $taxonomy ][ $term_id ] );
217 }
218 }
219 }
220 }
221
222 update_option( 'wpseo_taxonomy_meta', $taxonomies );
223 }
224 }
225
226 /**
227 * Performs upgrade functions to Yoast SEO 2.2.
228 *
229 * @return void
230 */
231 private function upgrade_22() {
232 // Unschedule our tracking.
233 wp_clear_scheduled_hook( 'yoast_tracking' );
234
235 $this->cleanup_option_data( 'wpseo' );
236 }
237
238 /**
239 * Schedules upgrade function to Yoast SEO 2.3.
240 *
241 * @return void
242 */
243 private function upgrade_23() {
244 add_action( 'wp', [ $this, 'upgrade_23_query' ], 90 );
245 add_action( 'admin_head', [ $this, 'upgrade_23_query' ], 90 );
246 }
247
248 /**
249 * Performs upgrade query to Yoast SEO 2.3.
250 *
251 * @return void
252 */
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.
256 $wp_query = new WP_Query( 'post_type=any&meta_key=_yoast_wpseo_sitemap-include&meta_value=never&order=ASC' );
257
258 if ( ! empty( $wp_query->posts ) ) {
259 $options = get_option( 'wpseo_xml' );
260
261 $excluded_posts = [];
262 if ( $options['excluded-posts'] !== '' ) {
263 $excluded_posts = explode( ',', $options['excluded-posts'] );
264 }
265
266 foreach ( $wp_query->posts as $post ) {
267 if ( ! in_array( (string) $post->ID, $excluded_posts, true ) ) {
268 $excluded_posts[] = $post->ID;
269 }
270 }
271
272 // Updates the meta value.
273 $options['excluded-posts'] = implode( ',', $excluded_posts );
274
275 // Update the option.
276 update_option( 'wpseo_xml', $options );
277 }
278
279 // Remove the meta fields.
280 delete_post_meta_by_key( '_yoast_wpseo_sitemap-include' );
281 }
282
283 /**
284 * Performs upgrade functions to Yoast SEO 3.0.
285 *
286 * @return void
287 */
288 private function upgrade_30() {
289 // Remove the meta fields for sitemap prio.
290 delete_post_meta_by_key( '_yoast_wpseo_sitemap-prio' );
291 }
292
293 /**
294 * Performs upgrade functions to Yoast SEO 3.3.
295 *
296 * @return void
297 */
298 private function upgrade_33() {
299 // Notification dismissals have been moved to User Meta instead of global option.
300 delete_option( Yoast_Notification_Center::STORAGE_KEY );
301 }
302
303 /**
304 * Performs upgrade functions to Yoast SEO 3.6.
305 *
306 * @return void
307 */
308 protected function upgrade_36() {
309 global $wpdb;
310
311 // Between 3.2 and 3.4 the sitemap options were saved with autoloading enabled.
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 );
320 }
321
322 /**
323 * Removes the about notice when its still in the database.
324 *
325 * @return void
326 */
327 private function upgrade_40() {
328 $center = Yoast_Notification_Center::get();
329 $center->remove_notification_by_id( 'wpseo-dismiss-about' );
330 }
331
332 /**
333 * Moves the content-analysis-active and keyword-analysis-acive options from wpseo-titles to wpseo.
334 *
335 * @return void
336 */
337 private function upgrade_44() {
338 $wpseo_titles = $this->get_option_from_database( 'wpseo_titles' );
339
340 $this->save_option_setting( $wpseo_titles, 'content-analysis-active', 'content_analysis_active' );
341 $this->save_option_setting( $wpseo_titles, 'keyword-analysis-active', 'keyword_analysis_active' );
342
343 // Remove irrelevant content from the option.
344 $this->cleanup_option_data( 'wpseo_titles' );
345 }
346
347 /**
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
351 */
352 private function upgrade_47() {
353 global $wpdb;
354
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.
358 $wpdb->query(
359 $wpdb->prepare(
360 'UPDATE ' . $wpdb->postmeta . ' SET meta_key = %s WHERE meta_key = "yst_is_cornerstone"',
361 WPSEO_Cornerstone_Filter::META_NAME,
362 ),
363 );
364 }
365
366 /**
367 * Removes the 'wpseo-dismiss-about' notice for every user that still has it.
368 *
369 * @return void
370 */
371 protected function upgrade_49() {
372 global $wpdb;
373
374 /*
375 * Using a filter to remove the notification for the current logged in user. The notification center is
376 * initializing the notifications before the upgrade routine has been executedd and is saving the stored
377 * notifications on shutdown. This causes the returning notification. By adding this filter the shutdown
378 * routine on the notification center will remove the notification.
379 */
380 add_filter( 'yoast_notifications_before_storage', [ $this, 'remove_about_notice' ] );
381
382 $meta_key = $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY;
383
384 // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
385 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
386 $usermetas = $wpdb->get_results(
387 $wpdb->prepare(
388 '
389 SELECT %i, %i
390 FROM %i
391 WHERE %i = %s AND %i LIKE %s
392 ',
393 [
394 'user_id',
395 'meta_value',
396 $wpdb->usermeta,
397 'meta_key',
398 $meta_key,
399 'meta_value',
400 '%wpseo-dismiss-about%',
401 ],
402 ),
403 ARRAY_A,
404 );
405
406 if ( empty( $usermetas ) ) {
407 return;
408 }
409
410 foreach ( $usermetas as $usermeta ) {
411 $notifications = maybe_unserialize( $usermeta['meta_value'] );
412
413 foreach ( $notifications as $notification_key => $notification ) {
414 if ( ! empty( $notification['options']['id'] ) && $notification['options']['id'] === 'wpseo-dismiss-about' ) {
415 unset( $notifications[ $notification_key ] );
416 }
417 }
418
419 update_user_option( $usermeta['user_id'], Yoast_Notification_Center::STORAGE_KEY, array_values( $notifications ) );
420 }
421 }
422
423 /**
424 * Removes the wpseo-dismiss-about notice from a list of notifications.
425 *
426 * @param Yoast_Notification[] $notifications The notifications to filter.
427 *
428 * @return Yoast_Notification[] The filtered list of notifications. Excluding the wpseo-dismiss-about notification.
429 */
430 public function remove_about_notice( $notifications ) {
431 foreach ( $notifications as $notification_key => $notification ) {
432 if ( $notification->get_id() === 'wpseo-dismiss-about' ) {
433 unset( $notifications[ $notification_key ] );
434 }
435 }
436
437 return $notifications;
438 }
439
440 /**
441 * Adds the yoast_seo_links table to the database.
442 *
443 * @return void
444 */
445 protected function upgrade_50() {
446 global $wpdb;
447
448 // Deletes the post meta value, which might created in the RC.
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 );
458 }
459
460 /**
461 * Register new capabilities and roles.
462 *
463 * @return void
464 */
465 private function upgrade_55() {
466 // Register roles.
467 do_action( 'wpseo_register_roles' );
468 WPSEO_Role_Manager_Factory::get()->add();
469
470 // Register capabilities.
471 do_action( 'wpseo_register_capabilities' );
472 WPSEO_Capability_Manager_Factory::get()->add();
473 }
474
475 /**
476 * Removes some no longer used options for noindexing subpages and for meta keywords and its associated templates.
477 *
478 * @return void
479 */
480 private function upgrade_63() {
481 $this->cleanup_option_data( 'wpseo_titles' );
482 }
483
484 /**
485 * Perform the 7.0 upgrade, moves settings around, deletes several options.
486 *
487 * @return void
488 */
489 private function upgrade_70() {
490
491 $wpseo_permalinks = $this->get_option_from_database( 'wpseo_permalinks' );
492 $wpseo_xml = $this->get_option_from_database( 'wpseo_xml' );
493 $wpseo_rss = $this->get_option_from_database( 'wpseo_rss' );
494 $wpseo = $this->get_option_from_database( 'wpseo' );
495 $wpseo_internallinks = $this->get_option_from_database( 'wpseo_internallinks' );
496
497 // Move some permalink settings, then delete the option.
498 $this->save_option_setting( $wpseo_permalinks, 'redirectattachment', 'disable-attachment' );
499 $this->save_option_setting( $wpseo_permalinks, 'stripcategorybase' );
500
501 // Move one XML sitemap setting, then delete the option.
502 $this->save_option_setting( $wpseo_xml, 'enablexmlsitemap', 'enable_xml_sitemap' );
503
504 // Move the RSS settings to the search appearance settings, then delete the RSS option.
505 $this->save_option_setting( $wpseo_rss, 'rssbefore' );
506 $this->save_option_setting( $wpseo_rss, 'rssafter' );
507
508 $this->save_option_setting( $wpseo, 'company_logo' );
509 $this->save_option_setting( $wpseo, 'company_name' );
510 $this->save_option_setting( $wpseo, 'company_or_person' );
511 $this->save_option_setting( $wpseo, 'person_name' );
512
513 // Remove the website name and altername name as we no longer need them.
514 $this->cleanup_option_data( 'wpseo' );
515
516 // All the breadcrumbs settings have moved to the search appearance settings.
517 foreach ( array_keys( $wpseo_internallinks ) as $key ) {
518 $this->save_option_setting( $wpseo_internallinks, $key );
519 }
520
521 // Convert hidden metabox options to display metabox options.
522 $title_options = get_option( 'wpseo_titles' );
523
524 foreach ( $title_options as $key => $value ) {
525 if ( strpos( $key, 'hideeditbox-tax-' ) === 0 ) {
526 $taxonomy = substr( $key, strlen( 'hideeditbox-tax-' ) );
527 WPSEO_Options::set( 'display-metabox-tax-' . $taxonomy, ! $value );
528 continue;
529 }
530
531 if ( strpos( $key, 'hideeditbox-' ) === 0 ) {
532 $post_type = substr( $key, strlen( 'hideeditbox-' ) );
533 WPSEO_Options::set( 'display-metabox-pt-' . $post_type, ! $value );
534 continue;
535 }
536 }
537
538 // Cleanup removed options.
539 delete_option( 'wpseo_xml' );
540 delete_option( 'wpseo_permalinks' );
541 delete_option( 'wpseo_rss' );
542 delete_option( 'wpseo_internallinks' );
543
544 // Remove possibly present plugin conflict notice for plugin that was removed from the list of conflicting plugins.
545 $yoast_plugin_conflict = WPSEO_Plugin_Conflict::get_instance();
546 $yoast_plugin_conflict->clear_error( 'header-footer/plugin.php' );
547
548 // Moves the user meta for excluding from the XML sitemap to a noindex.
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.
552 $wpdb->query( "UPDATE $wpdb->usermeta SET meta_key = 'wpseo_noindex_author' WHERE meta_key = 'wpseo_excludeauthorsitemap'" );
553 }
554
555 /**
556 * Perform the 7.1 upgrade.
557 *
558 * @return void
559 */
560 private function upgrade_71() {
561 $this->cleanup_option_data( 'wpseo_social' );
562
563 // Move the breadcrumbs setting and invert it.
564 $title_options = $this->get_option_from_database( 'wpseo_titles' );
565
566 if ( array_key_exists( 'breadcrumbs-blog-remove', $title_options ) ) {
567 WPSEO_Options::set( 'breadcrumbs-display-blog-page', ! $title_options['breadcrumbs-blog-remove'] );
568
569 $this->cleanup_option_data( 'wpseo_titles' );
570 }
571 }
572
573 /**
574 * Perform the 7.3 upgrade.
575 *
576 * @return void
577 */
578 private function upgrade_73() {
579 global $wpdb;
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.
583 $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_yoast_wpseo_is_cornerstone' WHERE meta_key = '_yst_is_cornerstone'" );
584
585 // Remove the previous Whip dismissed message, as this is a new one regarding PHP 5.2.
586 delete_option( 'whip_dismiss_timestamp' );
587 }
588
589 /**
590 * Performs the 7.4 upgrade.
591 *
592 * @return void
593 */
594 protected function upgrade_74() {
595 $this->remove_sitemap_validators();
596 }
597
598 /**
599 * Performs the 7.5.3 upgrade.
600 *
601 * When upgrading purging media is potentially relevant.
602 *
603 * @return void
604 */
605 private function upgrade_753() {
606 // Only when attachments are not disabled.
607 if ( WPSEO_Options::get( 'disable-attachment' ) === true ) {
608 return;
609 }
610
611 // Only when attachments are not no-indexed.
612 if ( WPSEO_Options::get( 'noindex-attachment' ) === true ) {
613 return;
614 }
615
616 // Set purging relevancy.
617 WPSEO_Options::set( 'is-media-purge-relevant', true );
618 }
619
620 /**
621 * Performs the 7.7 upgrade.
622 *
623 * @return void
624 */
625 private function upgrade_77() {
626 // Remove all OpenGraph content image cache.
627 $this->delete_post_meta( '_yoast_wpseo_post_image_cache' );
628 }
629
630 /**
631 * Performs the 7.7.2 upgrade.
632 *
633 * @return void
634 */
635 private function upgrade_772() {
636 if ( YoastSEO()->helpers->woocommerce->is_active() ) {
637 $this->migrate_woocommerce_archive_setting_to_shop_page();
638 }
639 }
640
641 /**
642 * Performs the 9.0 upgrade.
643 *
644 * @return void
645 */
646 protected function upgrade_90() {
647 global $wpdb;
648
649 // Invalidate all sitemap cache transients.
650 WPSEO_Sitemaps_Cache_Validator::cleanup_database();
651
652 // Removes all scheduled tasks for hitting the sitemap index.
653 wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' );
654
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 );
664 }
665
666 /**
667 * Performs the 10.0 upgrade.
668 *
669 * @return void
670 */
671 private function upgrade_100() {
672 // Removes recalibration notifications.
673 $this->clean_all_notifications();
674
675 // Removes recalibration options.
676 WPSEO_Options::clean_up( 'wpseo' );
677 delete_option( 'wpseo_recalibration_beta_mailinglist_subscription' );
678 }
679
680 /**
681 * Performs the 11.1 upgrade.
682 *
683 * @return void
684 */
685 private function upgrade_111() {
686 // Set company_or_person to company when it's an invalid value.
687 $company_or_person = WPSEO_Options::get( 'company_or_person', '' );
688
689 if ( ! in_array( $company_or_person, [ 'company', 'person' ], true ) ) {
690 WPSEO_Options::set( 'company_or_person', 'company' );
691 }
692 }
693
694 /**
695 * Performs the 12.3 upgrade.
696 *
697 * Removes the about notice when its still in the database.
698 *
699 * @return void
700 */
701 private function upgrade_123() {
702 $plugins = [
703 'yoast-seo-premium',
704 'video-seo-for-wordpress-seo-by-yoast',
705 'yoast-news-seo',
706 'local-seo-for-yoast-seo',
707 'yoast-woocommerce-seo',
708 'yoast-acf-analysis',
709 ];
710
711 $center = Yoast_Notification_Center::get();
712 foreach ( $plugins as $plugin ) {
713 $center->remove_notification_by_id( 'wpseo-outdated-yoast-seo-plugin-' . $plugin );
714 }
715 }
716
717 /**
718 * Performs the 12.4 upgrade.
719 *
720 * Removes the Google plus defaults from the database.
721 *
722 * @return void
723 */
724 private function upgrade_124() {
725 $this->cleanup_option_data( 'wpseo_social' );
726 }
727
728 /**
729 * Performs the 12.5 upgrade.
730 *
731 * @return void
732 */
733 public function upgrade_125() {
734 // Disables the force rewrite title when the theme supports it through WordPress.
735 if ( WPSEO_Options::get( 'forcerewritetitle', false ) && current_theme_supports( 'title-tag' ) ) {
736 WPSEO_Options::set( 'forcerewritetitle', false );
737 }
738
739 global $wpdb;
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 );
749
750 // Removes the WordPress update notification, because it is no longer necessary when WordPress 5.3 is released.
751 $center = Yoast_Notification_Center::get();
752 $center->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
753 }
754
755 /**
756 * Performs the 12.8 upgrade.
757 *
758 * @return void
759 */
760 private function upgrade_128() {
761 // Re-save wpseo to make sure bf_banner_2019_dismissed key is gone.
762 $this->cleanup_option_data( 'wpseo' );
763
764 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-page_comments-notice' );
765 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-wordpress-upgrade' );
766 }
767
768 /**
769 * Performs the 13.2 upgrade.
770 *
771 * @return void
772 */
773 private function upgrade_132() {
774 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-tagline-notice' );
775 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-permalink-notice' );
776 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-onpageorg' );
777
778 // Transfers the onpage option value to the ryte option.
779 $ryte_option = get_option( 'wpseo_ryte' );
780 $onpage_option = get_option( 'wpseo_onpage' );
781 if ( ! $ryte_option && $onpage_option ) {
782 update_option( 'wpseo_ryte', $onpage_option );
783 delete_option( 'wpseo_onpage' );
784 }
785
786 // Changes onpage_indexability to ryte_indexability.
787 $wpseo_option = get_option( 'wpseo' );
788 if ( isset( $wpseo_option['onpage_indexability'] ) && ! isset( $wpseo_option['ryte_indexability'] ) ) {
789 $wpseo_option['ryte_indexability'] = $wpseo_option['onpage_indexability'];
790 unset( $wpseo_option['onpage_indexability'] );
791 update_option( 'wpseo', $wpseo_option );
792 }
793
794 if ( wp_next_scheduled( 'wpseo_ryte_fetch' ) ) {
795 wp_clear_scheduled_hook( 'wpseo_ryte_fetch' );
796 }
797
798 /*
799 * Re-register capabilities to add the new `view_site_health_checks`
800 * capability to the SEO Manager role.
801 */
802 do_action( 'wpseo_register_capabilities' );
803 WPSEO_Capability_Manager_Factory::get()->add();
804 }
805
806 /**
807 * Perform the 14.0.3 upgrade.
808 *
809 * @return void
810 */
811 private function upgrade_1403() {
812 WPSEO_Options::set( 'ignore_indexation_warning', false );
813 }
814
815 /**
816 * Performs the 14.1 upgrade.
817 *
818 * @return void
819 */
820 private function upgrade_141() {
821 /*
822 * These notifications are retrieved from storage on the `init` hook with
823 * priority 1. We need to remove them after they're retrieved.
824 */
825 add_action( 'init', [ $this, 'remove_notifications_for_141' ] );
826 add_action( 'init', [ $this, 'clean_up_private_taxonomies_for_141' ] );
827
828 $this->reset_permalinks_of_attachments_for_141();
829 }
830
831 /**
832 * Performs the 14.2 upgrade.
833 *
834 * Removes the yoast-acf-analysis notice when it's still in the database.
835 *
836 * @return void
837 */
838 private function upgrade_142() {
839 add_action( 'init', [ $this, 'remove_acf_notification_for_142' ] );
840 }
841
842 /**
843 * Performs the 14.5 upgrade.
844 *
845 * @return void
846 */
847 private function upgrade_145() {
848 add_action( 'init', [ $this, 'set_indexation_completed_option_for_145' ] );
849 }
850
851 /**
852 * Performs the 14.9 upgrade.
853 *
854 * @return void
855 */
856 private function upgrade_149() {
857 $version = get_option( 'wpseo_license_server_version', 2 );
858 WPSEO_Options::set( 'license_server_version', $version );
859 delete_option( 'wpseo_license_server_version' );
860 }
861
862 /**
863 * Performs the 15.1 upgrade.
864 *
865 * @return void
866 */
867 private function upgrade_151() {
868 $this->set_home_url_for_151();
869 $this->move_indexables_indexation_reason_for_151();
870
871 add_action( 'init', [ $this, 'set_permalink_structure_option_for_151' ] );
872 add_action( 'init', [ $this, 'store_custom_taxonomy_slugs_for_151' ] );
873 }
874
875 /**
876 * Performs the 15.3 upgrade.
877 *
878 * @return void
879 */
880 private function upgrade_153() {
881 WPSEO_Options::set( 'category_base_url', get_option( 'category_base' ) );
882 WPSEO_Options::set( 'tag_base_url', get_option( 'tag_base' ) );
883
884 // Rename a couple of options.
885 $indexation_started_value = WPSEO_Options::get( 'indexation_started' );
886 WPSEO_Options::set( 'indexing_started', $indexation_started_value );
887
888 $indexables_indexing_completed_value = WPSEO_Options::get( 'indexables_indexation_completed' );
889 WPSEO_Options::set( 'indexables_indexing_completed', $indexables_indexing_completed_value );
890 }
891
892 /**
893 * Performs the 15.5 upgrade.
894 *
895 * @return void
896 */
897 private function upgrade_155() {
898 // Unset the fbadminapp value in the wpseo_social option.
899 $wpseo_social_option = get_option( 'wpseo_social' );
900
901 if ( isset( $wpseo_social_option['fbadminapp'] ) ) {
902 unset( $wpseo_social_option['fbadminapp'] );
903 update_option( 'wpseo_social', $wpseo_social_option );
904 }
905 }
906
907 /**
908 * Performs the 15.7 upgrade.
909 *
910 * @return void
911 */
912 private function upgrade_157() {
913 add_action( 'init', [ $this, 'remove_plugin_updated_notification_for_157' ] );
914 }
915
916 /**
917 * Performs the 15.9.1 upgrade routine.
918 *
919 * @return void
920 */
921 private function upgrade_1591() {
922 $enabled_auto_updates = get_option( 'auto_update_plugins' );
923 $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
924 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', [], $enabled_auto_updates );
925 }
926
927 /**
928 * Performs the 16.2 upgrade routine.
929 *
930 * @return void
931 */
932 private function upgrade_162() {
933 $enabled_auto_updates = get_site_option( 'auto_update_plugins' );
934 $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
935 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
936 }
937
938 /**
939 * Performs the 16.5 upgrade.
940 *
941 * @return void
942 */
943 private function upgrade_165() {
944 add_action( 'init', [ $this, 'copy_og_settings_from_social_to_titles' ], 99 );
945
946 // Run after the WPSEO_Options::enrich_defaults method which has priority 99.
947 add_action( 'init', [ $this, 'reset_og_settings_to_default_values' ], 100 );
948 }
949
950 /**
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.
953 *
954 * @return void
955 */
956 private function upgrade_172() {
957 wp_unschedule_hook( 'wpseo_cleanup_orphaned_indexables' );
958 wp_unschedule_hook( 'wpseo_cleanup_indexables' );
959
960 if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
961 wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
962 }
963 }
964
965 /**
966 * Performs the 17.7.1 upgrade routine.
967 *
968 * @return void
969 */
970 private function upgrade_1771() {
971 $enabled_auto_updates = get_site_option( 'auto_update_plugins' );
972 $addon_update_watcher = YoastSEO()->classes->get( Addon_Update_Watcher::class );
973 $addon_update_watcher->toggle_auto_updates_for_add_ons( 'auto_update_plugins', $enabled_auto_updates, [] );
974 }
975
976 /**
977 * Performs the 17.9 upgrade routine.
978 *
979 * @return void
980 */
981 private function upgrade_179() {
982 WPSEO_Options::set( 'wincher_integration_active', true );
983 }
984
985 /**
986 * Performs the 18.3 upgrade routine.
987 *
988 * @return void
989 */
990 private function upgrade_183() {
991 $this->delete_post_meta( 'yoast-structured-data-blocks-images-cache' );
992 }
993
994 /**
995 * Performs the 18.6 upgrade routine.
996 *
997 * @return void
998 */
999 private function upgrade_186() {
1000 if ( is_multisite() ) {
1001 WPSEO_Options::set( 'allow_wincher_integration_active', false );
1002 }
1003 }
1004
1005 /**
1006 * Performs the 18.9 upgrade routine.
1007 *
1008 * @return void
1009 */
1010 private function upgrade_189() {
1011 // Make old users not get the Installation Success page after upgrading.
1012 WPSEO_Options::set( 'should_redirect_after_install_free', false );
1013 // We're adding a hardcoded time here, so that in the future we can be able to identify whether the user did see the Installation Success page or not.
1014 // If they did, they wouldn't have this hardcoded value in that option, but rather (roughly) the timestamp of the moment they saw it.
1015 WPSEO_Options::set( 'activation_redirect_timestamp_free', 1_652_258_756 );
1016
1017 // Transfer the Social URLs.
1018 $other = [];
1019 $other[] = WPSEO_Options::get( 'instagram_url' );
1020 $other[] = WPSEO_Options::get( 'linkedin_url' );
1021 $other[] = WPSEO_Options::get( 'myspace_url' );
1022 $other[] = WPSEO_Options::get( 'pinterest_url' );
1023 $other[] = WPSEO_Options::get( 'youtube_url' );
1024 $other[] = WPSEO_Options::get( 'wikipedia_url' );
1025
1026 WPSEO_Options::set( 'other_social_urls', array_values( array_unique( array_filter( $other ) ) ) );
1027
1028 // Transfer the progress of the old Configuration Workout.
1029 $workout_data = WPSEO_Options::get( 'workouts_data' );
1030 $old_conf_progress = ( $workout_data['configuration']['finishedSteps'] ?? [] );
1031
1032 if ( in_array( 'optimizeSeoData', $old_conf_progress, true ) && in_array( 'siteRepresentation', $old_conf_progress, true ) ) {
1033 // If completed ‘SEO optimization’ and ‘Site representation’ step, we assume the workout was completed.
1034 $configuration_finished_steps = [
1035 'siteRepresentation',
1036 'socialProfiles',
1037 'personalPreferences',
1038 ];
1039 WPSEO_Options::set( 'configuration_finished_steps', $configuration_finished_steps );
1040 }
1041 }
1042
1043 /**
1044 * Performs the 19.1 upgrade routine.
1045 *
1046 * @return void
1047 */
1048 private function upgrade_191() {
1049 if ( is_multisite() ) {
1050 WPSEO_Options::set( 'allow_remove_feed_post_comments', true );
1051 }
1052 }
1053
1054 /**
1055 * Performs the 19.3 upgrade routine.
1056 *
1057 * @return void
1058 */
1059 private function upgrade_193() {
1060 if ( empty( get_option( 'wpseo_premium', [] ) ) ) {
1061 WPSEO_Options::set( 'enable_index_now', true );
1062 WPSEO_Options::set( 'enable_link_suggestions', true );
1063 }
1064 }
1065
1066 /**
1067 * Performs the 19.6 upgrade routine.
1068 *
1069 * @return void
1070 */
1071 private function upgrade_196() {
1072 WPSEO_Options::set( 'ryte_indexability', false );
1073 WPSEO_Options::set( 'allow_ryte_indexability', false );
1074 wp_clear_scheduled_hook( 'wpseo_ryte_fetch' );
1075 }
1076
1077 /**
1078 * Performs the 19.11 upgrade routine.
1079 *
1080 * @return void
1081 */
1082 private function upgrade_1911() {
1083 add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_post_types' ] );
1084 add_action( 'shutdown', [ $this, 'remove_indexable_rows_for_non_public_taxonomies' ] );
1085 $this->deduplicate_unindexed_indexable_rows();
1086 $this->remove_indexable_rows_for_disabled_authors_archive();
1087 if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1088 wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1089 }
1090 }
1091
1092 /**
1093 * Performs the 20.2 upgrade routine.
1094 *
1095 * @return void
1096 */
1097 private function upgrade_202() {
1098 if ( WPSEO_Options::get( 'disable-attachment', true ) ) {
1099 $attachment_cleanup_helper = YoastSEO()->helpers->attachment_cleanup;
1100
1101 $attachment_cleanup_helper->remove_attachment_indexables( true );
1102 $attachment_cleanup_helper->clean_attachment_links_from_target_indexable_ids( true );
1103 }
1104
1105 $this->clean_unindexed_indexable_rows_with_no_object_id();
1106
1107 if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1108 // This schedules the cleanup routine cron again, since in combination of premium cleans up the prominent words table. We also want to cleanup possible orphaned hierarchies from the above cleanups.
1109 wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1110 }
1111 }
1112
1113 /**
1114 * Performs the 20.5 upgrade routine.
1115 *
1116 * @return void
1117 */
1118 private function upgrade_205() {
1119 if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1120 wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1121 }
1122 }
1123
1124 /**
1125 * Performs the 20.7 upgrade routine.
1126 * Removes the metadata related to the settings page introduction modal for all the users.
1127 * Also, schedules another cleanup scheduled action.
1128 *
1129 * @return void
1130 */
1131 private function upgrade_207() {
1132 add_action( 'shutdown', [ $this, 'delete_user_introduction_meta' ] );
1133 }
1134
1135 /**
1136 * Performs the 20.8 upgrade routine.
1137 * Schedules another cleanup scheduled action.
1138 *
1139 * @return void
1140 */
1141 private function upgrade_208() {
1142 if ( ! wp_next_scheduled( Cleanup_Integration::START_HOOK ) ) {
1143 wp_schedule_single_event( ( time() + ( MINUTE_IN_SECONDS * 5 ) ), Cleanup_Integration::START_HOOK );
1144 }
1145 }
1146
1147 /**
1148 * Performs the 22.6 upgrade routine.
1149 * Schedules another cleanup scheduled action, but starting from the last cleanup action we just added (if there
1150 * aren't any running cleanups already).
1151 *
1152 * @return void
1153 */
1154 private function upgrade_226() {
1155 if ( get_option( Cleanup_Integration::CURRENT_TASK_OPTION ) === false ) {
1156 $cleanup_integration = YoastSEO()->classes->get( Cleanup_Integration::class );
1157 $cleanup_integration->start_cron_job( 'clean_selected_empty_usermeta', DAY_IN_SECONDS );
1158 }
1159 }
1160
1161 /**
1162 * Sets the home_url option for the 15.1 upgrade routine.
1163 *
1164 * @return void
1165 */
1166 protected function set_home_url_for_151() {
1167 $home_url = WPSEO_Options::get( 'home_url' );
1168
1169 if ( empty( $home_url ) ) {
1170 WPSEO_Options::set( 'home_url', get_home_url() );
1171 }
1172 }
1173
1174 /**
1175 * Moves the `indexables_indexation_reason` option to the
1176 * renamed `indexing_reason` option.
1177 *
1178 * @return void
1179 */
1180 protected function move_indexables_indexation_reason_for_151() {
1181 $reason = WPSEO_Options::get( 'indexables_indexation_reason', '' );
1182 WPSEO_Options::set( 'indexing_reason', $reason );
1183 }
1184
1185 /**
1186 * Checks if the indexable indexation is completed.
1187 * If so, sets the `indexables_indexation_completed` option to `true`,
1188 * else to `false`.
1189 *
1190 * @return void
1191 */
1192 public function set_indexation_completed_option_for_145() {
1193 WPSEO_Options::set( 'indexables_indexation_completed', YoastSEO()->helpers->indexing->get_limited_filtered_unindexed_count( 1 ) === 0 );
1194 }
1195
1196 /**
1197 * Cleans up the private taxonomies from the indexables table for the upgrade routine to 14.1.
1198 *
1199 * @return void
1200 */
1201 public function clean_up_private_taxonomies_for_141() {
1202 global $wpdb;
1203
1204 // If migrations haven't been completed successfully the following may give false errors. So suppress them.
1205 $show_errors = $wpdb->show_errors;
1206 $wpdb->show_errors = false;
1207
1208 // Clean up indexables of private taxonomies.
1209 $private_taxonomies = get_taxonomies( [ 'public' => false ], 'names' );
1210
1211 if ( empty( $private_taxonomies ) ) {
1212 return;
1213 }
1214
1215 $replacements = array_merge(
1216 [
1217 Model::get_table_name( 'Indexable' ),
1218 'object_type',
1219 'object_sub_type',
1220 ],
1221 $private_taxonomies,
1222 );
1223
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' ) )
1232 . ')',
1233 $replacements,
1234 ),
1235 );
1236
1237 $wpdb->show_errors = $show_errors;
1238 }
1239
1240 /**
1241 * Resets the permalinks of attachments to `null` in the indexable table for the upgrade routine to 14.1.
1242 *
1243 * @return void
1244 */
1245 private function reset_permalinks_of_attachments_for_141() {
1246 global $wpdb;
1247
1248 // If migrations haven't been completed succesfully the following may give false errors. So suppress them.
1249 $show_errors = $wpdb->show_errors;
1250 $wpdb->show_errors = false;
1251
1252 // Reset the permalinks of the attachments in the indexable table.
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 );
1261
1262 $wpdb->show_errors = $show_errors;
1263 }
1264
1265 /**
1266 * Removes notifications from the Notification center for the 14.1 upgrade.
1267 *
1268 * @return void
1269 */
1270 public function remove_notifications_for_141() {
1271 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-recalculate' );
1272 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-dismiss-blog-public-notice' );
1273 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-links-table-not-accessible' );
1274 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-post-type-archive-notification' );
1275 }
1276
1277 /**
1278 * Removes the wpseo-suggested-plugin-yoast-acf-analysis notification from the Notification center for the 14.2
1279 * upgrade.
1280 *
1281 * @return void
1282 */
1283 public function remove_acf_notification_for_142() {
1284 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-suggested-plugin-yoast-acf-analysis' );
1285 }
1286
1287 /**
1288 * Removes the wpseo-plugin-updated notification from the Notification center for the 15.7 upgrade.
1289 *
1290 * @return void
1291 */
1292 public function remove_plugin_updated_notification_for_157() {
1293 Yoast_Notification_Center::get()->remove_notification_by_id( 'wpseo-plugin-updated' );
1294 }
1295
1296 /**
1297 * Removes all notifications saved in the database under 'wp_yoast_notifications'.
1298 *
1299 * @return void
1300 */
1301 private function clean_all_notifications() {
1302 global $wpdb;
1303 delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . Yoast_Notification_Center::STORAGE_KEY, '', true );
1304 }
1305
1306 /**
1307 * Removes the post meta fields for a given meta key.
1308 *
1309 * @param string $meta_key The meta key.
1310 *
1311 * @return void
1312 */
1313 private function delete_post_meta( $meta_key ) {
1314 global $wpdb;
1315 $deleted = $wpdb->delete( $wpdb->postmeta, [ 'meta_key' => $meta_key ], [ '%s' ] );
1316
1317 if ( $deleted ) {
1318 wp_cache_set( 'last_changed', microtime(), 'posts' );
1319 }
1320 }
1321
1322 /**
1323 * Removes all sitemap validators.
1324 *
1325 * This should be executed on every upgrade routine until we have removed the sitemap caching in the database.
1326 *
1327 * @return void
1328 */
1329 private function remove_sitemap_validators() {
1330 global $wpdb;
1331
1332 // Remove all sitemap validators.
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 );
1341 }
1342
1343 /**
1344 * Retrieves the option value directly from the database.
1345 *
1346 * @param string $option_name Option to retrieve.
1347 *
1348 * @return int|string|bool|float|array<string|int|bool|float> The content of the option if exists, otherwise an
1349 * empty array.
1350 */
1351 protected function get_option_from_database( $option_name ) {
1352 global $wpdb;
1353
1354 // Load option directly from the database, to avoid filtering and sanitization.
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
1365 if ( ! empty( $results ) ) {
1366 return maybe_unserialize( $results[0]['option_value'] );
1367 }
1368
1369 return [];
1370 }
1371
1372 /**
1373 * Cleans the option to make sure only relevant settings are there.
1374 *
1375 * @param string $option_name Option name save.
1376 *
1377 * @return void
1378 */
1379 protected function cleanup_option_data( $option_name ) {
1380 $data = get_option( $option_name, [] );
1381 if ( ! is_array( $data ) || $data === [] ) {
1382 return;
1383 }
1384
1385 /*
1386 * Clean up the option by re-saving it.
1387 *
1388 * The option framework will remove any settings that are not configured
1389 * for this option, removing any migrated settings.
1390 */
1391 update_option( $option_name, $data );
1392 }
1393
1394 /**
1395 * Saves an option setting to where it should be stored.
1396 *
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.
1401 *
1402 * @return void
1403 */
1404 protected function save_option_setting( $source_data, $source_setting, $target_setting = null ) {
1405 $target_setting ??= $source_setting;
1406
1407 if ( isset( $source_data[ $source_setting ] ) ) {
1408 WPSEO_Options::set( $target_setting, $source_data[ $source_setting ] );
1409 }
1410 }
1411
1412 /**
1413 * Migrates WooCommerce archive settings to the WooCommerce Shop page meta-data settings.
1414 *
1415 * If no Shop page is defined, nothing will be migrated.
1416 *
1417 * @return void
1418 */
1419 private function migrate_woocommerce_archive_setting_to_shop_page() {
1420 $shop_page_id = wc_get_page_id( 'shop' );
1421
1422 if ( $shop_page_id === -1 ) {
1423 return;
1424 }
1425
1426 $title = WPSEO_Meta::get_value( 'title', $shop_page_id );
1427
1428 if ( empty( $title ) ) {
1429 $option_title = WPSEO_Options::get( 'title-ptarchive-product' );
1430
1431 WPSEO_Meta::set_value(
1432 'title',
1433 $option_title,
1434 $shop_page_id,
1435 );
1436
1437 WPSEO_Options::set( 'title-ptarchive-product', '' );
1438 }
1439
1440 $meta_description = WPSEO_Meta::get_value( 'metadesc', $shop_page_id );
1441
1442 if ( empty( $meta_description ) ) {
1443 $option_metadesc = WPSEO_Options::get( 'metadesc-ptarchive-product' );
1444
1445 WPSEO_Meta::set_value(
1446 'metadesc',
1447 $option_metadesc,
1448 $shop_page_id,
1449 );
1450
1451 WPSEO_Options::set( 'metadesc-ptarchive-product', '' );
1452 }
1453
1454 $bc_title = WPSEO_Meta::get_value( 'bctitle', $shop_page_id );
1455
1456 if ( empty( $bc_title ) ) {
1457 $option_bctitle = WPSEO_Options::get( 'bctitle-ptarchive-product' );
1458
1459 WPSEO_Meta::set_value(
1460 'bctitle',
1461 $option_bctitle,
1462 $shop_page_id,
1463 );
1464
1465 WPSEO_Options::set( 'bctitle-ptarchive-product', '' );
1466 }
1467
1468 $noindex = WPSEO_Meta::get_value( 'meta-robots-noindex', $shop_page_id );
1469
1470 if ( $noindex === '0' ) {
1471 $option_noindex = WPSEO_Options::get( 'noindex-ptarchive-product' );
1472
1473 WPSEO_Meta::set_value(
1474 'meta-robots-noindex',
1475 $option_noindex,
1476 $shop_page_id,
1477 );
1478
1479 WPSEO_Options::set( 'noindex-ptarchive-product', false );
1480 }
1481 }
1482
1483 /**
1484 * Stores the initial `permalink_structure` option.
1485 *
1486 * @return void
1487 */
1488 public function set_permalink_structure_option_for_151() {
1489 WPSEO_Options::set( 'permalink_structure', get_option( 'permalink_structure' ) );
1490 }
1491
1492 /**
1493 * Stores the initial slugs of custom taxonomies.
1494 *
1495 * @return void
1496 */
1497 public function store_custom_taxonomy_slugs_for_151() {
1498 $taxonomies = $this->taxonomy_helper->get_custom_taxonomies();
1499
1500 $custom_taxonomies = [];
1501
1502 foreach ( $taxonomies as $taxonomy ) {
1503 $slug = $this->taxonomy_helper->get_taxonomy_slug( $taxonomy );
1504
1505 $custom_taxonomies[ $taxonomy ] = $slug;
1506 }
1507
1508 WPSEO_Options::set( 'custom_taxonomy_slugs', $custom_taxonomies );
1509 }
1510
1511 /**
1512 * Copies the frontpage social settings to the titles options.
1513 *
1514 * @return void
1515 */
1516 public function copy_og_settings_from_social_to_titles() {
1517 $wpseo_social = get_option( 'wpseo_social' );
1518 $wpseo_titles = get_option( 'wpseo_titles' );
1519
1520 $copied_options = [];
1521 // Reset to the correct default value.
1522 $copied_options['open_graph_frontpage_title'] = '%%sitename%%';
1523
1524 $options = [
1525 'og_frontpage_title' => 'open_graph_frontpage_title',
1526 'og_frontpage_desc' => 'open_graph_frontpage_desc',
1527 'og_frontpage_image' => 'open_graph_frontpage_image',
1528 'og_frontpage_image_id' => 'open_graph_frontpage_image_id',
1529 ];
1530
1531 foreach ( $options as $social_option => $titles_option ) {
1532 if ( ! empty( $wpseo_social[ $social_option ] ) ) {
1533 $copied_options[ $titles_option ] = $wpseo_social[ $social_option ];
1534 }
1535 }
1536
1537 $wpseo_titles = array_merge( $wpseo_titles, $copied_options );
1538
1539 update_option( 'wpseo_titles', $wpseo_titles );
1540 }
1541
1542 /**
1543 * Reset the social options with the correct default values.
1544 *
1545 * @return void
1546 */
1547 public function reset_og_settings_to_default_values() {
1548 $wpseo_titles = get_option( 'wpseo_titles' );
1549 $updated_options = [];
1550
1551 $updated_options['social-title-author-wpseo'] = '%%name%%';
1552 $updated_options['social-title-archive-wpseo'] = '%%date%%';
1553
1554 /* translators: %s expands to the name of a post type (plural). */
1555 $post_type_archive_default = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' );
1556
1557 /* translators: %s expands to the variable used for term title. */
1558 $term_archive_default = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' );
1559
1560 $post_type_objects = get_post_types( [ 'public' => true ], 'objects' );
1561
1562 if ( $post_type_objects ) {
1563 foreach ( $post_type_objects as $pt ) {
1564 // Post types.
1565 if ( isset( $wpseo_titles[ 'social-title-' . $pt->name ] ) ) {
1566 $updated_options[ 'social-title-' . $pt->name ] = '%%title%%';
1567 }
1568 // Post type archives.
1569 if ( isset( $wpseo_titles[ 'social-title-ptarchive-' . $pt->name ] ) ) {
1570 $updated_options[ 'social-title-ptarchive-' . $pt->name ] = $post_type_archive_default;
1571 }
1572 }
1573 }
1574
1575 $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' );
1576
1577 if ( $taxonomy_objects ) {
1578 foreach ( $taxonomy_objects as $tax ) {
1579 if ( isset( $wpseo_titles[ 'social-title-tax-' . $tax->name ] ) ) {
1580 $updated_options[ 'social-title-tax-' . $tax->name ] = $term_archive_default;
1581 }
1582 }
1583 }
1584
1585 $wpseo_titles = array_merge( $wpseo_titles, $updated_options );
1586
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 );
1870 }
1871 }
1872