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-addon-manager.php +121 -56 18.228.5 View file →
@@ -4,8 +4,12 @@
4 4 *
5 5 * @package WPSEO\Inc
6 6 */
7 7
8 +use Yoast\WP\SEO\General\User_Interface\General_Page_Integration;
9 +use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration;
10 +use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
11 +
8 12 /**
9 13 * Represents the addon manager.
10 14 */
11 15 class WPSEO_Addon_Manager {
@@ -14,9 +18,9 @@
14 18 * Holds the name of the transient.
15 19 *
16 20 * @var string
17 21 */
18 - const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
22 + public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
19 23
20 24 /**
21 25 * Holds the name of the transient.
22 26 *
@@ -21,9 +25,9 @@
21 25 * Holds the name of the transient.
22 26 *
23 27 * @var string
24 28 */
25 - const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick';
29 + public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick';
26 30
27 31 /**
28 32 * Holds the slug for YoastSEO free.
29 33 *
@@ -28,9 +32,9 @@
28 32 * Holds the slug for YoastSEO free.
29 33 *
30 34 * @var string
31 35 */
32 - const FREE_SLUG = 'yoast-seo-wordpress';
36 + public const FREE_SLUG = 'yoast-seo-wordpress';
33 37
34 38 /**
35 39 * Holds the slug for YoastSEO Premium.
36 40 *
@@ -35,9 +39,9 @@
35 39 * Holds the slug for YoastSEO Premium.
36 40 *
37 41 * @var string
38 42 */
39 - const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
43 + public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
40 44
41 45 /**
42 46 * Holds the slug for Yoast News.
43 47 *
@@ -42,9 +46,9 @@
42 46 * Holds the slug for Yoast News.
43 47 *
44 48 * @var string
45 49 */
46 - const NEWS_SLUG = 'yoast-seo-news';
50 + public const NEWS_SLUG = 'yoast-seo-news';
47 51
48 52 /**
49 53 * Holds the slug for Video.
50 54 *
@@ -49,9 +53,9 @@
49 53 * Holds the slug for Video.
50 54 *
51 55 * @var string
52 56 */
53 - const VIDEO_SLUG = 'yoast-seo-video';
57 + public const VIDEO_SLUG = 'yoast-seo-video';
54 58
55 59 /**
56 60 * Holds the slug for WooCommerce.
57 61 *
@@ -56,9 +60,9 @@
56 60 * Holds the slug for WooCommerce.
57 61 *
58 62 * @var string
59 63 */
60 - const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
64 + public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
61 65
62 66 /**
63 67 * Holds the slug for Local.
64 68 *
@@ -63,14 +67,14 @@
63 67 * Holds the slug for Local.
64 68 *
65 69 * @var string
66 70 */
67 - const LOCAL_SLUG = 'yoast-seo-local';
71 + public const LOCAL_SLUG = 'yoast-seo-local';
68 72
69 73 /**
70 74 * The expected addon data.
71 75 *
72 - * @var array
76 + * @var array<string, string>
73 77 */
74 78 protected static $addons = [
75 79 'wp-seo-premium.php' => self::PREMIUM_SLUG,
76 80 'wpseo-news.php' => self::NEWS_SLUG,
@@ -79,8 +83,41 @@
79 83 'local-seo.php' => self::LOCAL_SLUG,
80 84 ];
81 85
82 86 /**
87 + * The addon data for the shortlinks.
88 + *
89 + * @var array<string, array<string, string>>
90 + */
91 + private $addon_details = [
92 + self::PREMIUM_SLUG => [
93 + 'name' => 'Yoast SEO Premium',
94 + 'short_link_activation' => 'https://yoa.st/13j',
95 + 'short_link_renewal' => 'https://yoa.st/4ey',
96 + ],
97 + self::NEWS_SLUG => [
98 + 'name' => 'Yoast News SEO',
99 + 'short_link_activation' => 'https://yoa.st/4xq',
100 + 'short_link_renewal' => 'https://yoa.st/4xv',
101 + ],
102 + self::WOOCOMMERCE_SLUG => [
103 + 'name' => 'Yoast WooCommerce SEO',
104 + 'short_link_activation' => 'https://yoa.st/4xs',
105 + 'short_link_renewal' => 'https://yoa.st/4xx',
106 + ],
107 + self::VIDEO_SLUG => [
108 + 'name' => 'Yoast Video SEO',
109 + 'short_link_activation' => 'https://yoa.st/4xr',
110 + 'short_link_renewal' => 'https://yoa.st/4xw',
111 + ],
112 + self::LOCAL_SLUG => [
113 + 'name' => 'Yoast Local SEO',
114 + 'short_link_activation' => 'https://yoa.st/4xp',
115 + 'short_link_renewal' => 'https://yoa.st/4xu',
116 + ],
117 + ];
118 +
119 + /**
83 120 * Holds the site information data.
84 121 *
85 122 * @var stdClass
86 123 */
@@ -173,9 +210,9 @@
173 210
174 211 /**
175 212 * Retrieves a list of (subscription) slugs by the active addons.
176 213 *
177 - * @return array The slugs.
214 + * @return array<string, stdClass> The slugs.
178 215 */
179 216 public function get_subscriptions_for_active_addons() {
180 217 $active_addons = array_keys( $this->get_active_addons() );
181 218 $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
@@ -189,9 +226,9 @@
189 226
190 227 /**
191 228 * Retrieves a list of versions for each addon.
192 229 *
193 - * @return array The addon versions.
230 + * @return array<string, string> The addon versions.
194 231 */
195 232 public function get_installed_addons_versions() {
196 233 $addon_versions = [];
197 234 foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
@@ -238,11 +275,9 @@
238 275 *
239 276 * @return stdClass The list of addons activated for this site.
240 277 */
241 278 public function get_myyoast_site_information() {
242 - if ( $this->site_information === null ) {
243 - $this->site_information = $this->get_site_information_transient();
244 - }
279 + $this->site_information ??= $this->get_site_information_transient();
245 280
246 281 if ( $this->site_information ) {
247 282 return $this->site_information;
248 283 }
@@ -279,9 +314,9 @@
279 314
280 315 /**
281 316 * Checks if there are addon updates.
282 317 *
283 - * @param stdClass|mixed $data The current data for update_plugins.
318 + * @param stdClass $data The current data for update_plugins.
284 319 *
285 320 * @return stdClass Extended data for update_plugins.
286 321 */
287 322 public function check_for_updates( $data ) {
@@ -309,9 +344,9 @@
309 344
310 345 // If the add-on's version is the latest, we have to do no further checks.
311 346 if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) {
312 347 // If we haven't retrieved the Yoast Free requirements for the WP version yet, do nothing. The next run will probably get us that information.
313 - if ( is_null( $plugin_data->requires ) ) {
348 + if ( $plugin_data->requires === null ) {
314 349 continue;
315 350 }
316 351
317 352 if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
@@ -360,15 +395,37 @@
360 395 /**
361 396 * If the plugin is lacking an active subscription, throw a warning.
362 397 *
363 398 * @param array $plugin_data The data for the plugin in this row.
399 + *
400 + * @return void
364 401 */
365 402 public function expired_subscription_warning( $plugin_data ) {
366 403 $subscription = $this->get_subscription( $plugin_data['slug'] );
367 404 if ( $subscription && $this->has_subscription_expired( $subscription ) ) {
405 + $addon_link = ( isset( $this->addon_details[ $plugin_data['slug'] ] ) ) ? $this->addon_details[ $plugin_data['slug'] ]['short_link_renewal'] : $this->addon_details[ self::PREMIUM_SLUG ]['short_link_renewal'];
406 +
407 + $sale_copy = '';
408 + if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) {
409 + $sale_copy = sprintf(
410 + /* translators: %1$s and %2$s are a <span> opening and closing tag. */
411 + esc_html__( '%1$s30%% OFF - Black Friday %2$s', 'wordpress-seo' ),
412 + '<span class="yoast-update-plugin-bf-sale-badge">',
413 + '</span>',
414 + );
415 + }
368 416 echo '<br><br>';
369 - /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
370 - echo '<strong><span class="wp-ui-text-notification alert dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'A new version of %1$s is available. %2$sRenew your subscription%3$s if you want to update to the latest version.', 'wordpress-seo' ), esc_html( $plugin_data['name'] ), '<a href="' . esc_attr( WPSEO_Shortlinker::get( 'https://yoa.st/4ey' ) ) . '">', '</a>' ) . '</strong>';
417 + echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> '
418 + . sprintf(
419 + /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
420 + esc_html__( 'Your %1$s plugin cannot be updated as your subscription has expired. %2$sRenew your product subscription%3$s to restore updates and full feature access.', 'wordpress-seo' ),
421 + esc_html( $plugin_data['name'] ),
422 + '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">',
423 + '</a>',
424 + )
425 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
426 + . $sale_copy
427 + . '</strong>';
371 428 }
372 429 }
373 430
374 431 /**
@@ -406,8 +463,10 @@
406 463 }
407 464
408 465 /**
409 466 * Validates the addons and show a notice for the ones that are invalid.
467 + *
468 + * @return void
410 469 */
411 470 public function validate_addons() {
412 471 $notification_center = Yoast_Notification_Center::get();
413 472
@@ -414,19 +473,11 @@
414 473 if ( $notification_center === null ) {
415 474 return;
416 475 }
417 476
418 - $addons = [
419 - 'Yoast SEO Premium' => static::PREMIUM_SLUG,
420 - 'News SEO' => static::NEWS_SLUG,
421 - 'Yoast WooCommerce SEO' => static::WOOCOMMERCE_SLUG,
422 - 'Video SEO' => static::VIDEO_SLUG,
423 - 'Local SEO' => static::LOCAL_SLUG,
424 - ];
477 + foreach ( $this->addon_details as $slug => $addon_info ) {
478 + $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
425 479
426 - foreach ( $addons as $product_name => $slug ) {
427 - $notification = $this->create_notification( $product_name );
428 -
429 480 // Add a notification when the installed plugin isn't activated in My Yoast.
430 481 if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
431 482 $notification_center->add_notification( $notification );
432 483
@@ -437,8 +488,19 @@
437 488 }
438 489 }
439 490
440 491 /**
492 + * Checks if the user has any active addons.
493 + *
494 + * @return bool Whether there are active addons.
495 + */
496 + public function has_active_addons() {
497 + $active_addons = $this->get_active_addons();
498 +
499 + return ! empty( $active_addons );
500 + }
501 +
502 + /**
441 503 * Removes the site information transients.
442 504 *
443 505 * @codeCoverageIgnore
444 506 *
@@ -452,12 +514,13 @@
452 514 /**
453 515 * Creates an instance of Yoast_Notification.
454 516 *
455 517 * @param string $product_name The product to create the notification for.
518 + * @param string $short_link The short link for the addon notification.
456 519 *
457 520 * @return Yoast_Notification The created notification.
458 521 */
459 - protected function create_notification( $product_name ) {
522 + protected function create_notification( $product_name, $short_link ) {
460 523 $notification_options = [
461 524 'type' => Yoast_Notification::ERROR,
462 525 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
463 526 'capabilities' => 'wpseo_manage_options',
@@ -464,14 +527,19 @@
464 527 ];
465 528
466 529 return new Yoast_Notification(
467 530 sprintf(
468 - /* translators: %1$s expands to the product name. %2$s expands to a link to My Yoast */
469 - __( 'You are not receiving updates or support! Fix this problem by adding this site and enabling %1$s for it in %2$s.', 'wordpress-seo' ),
531 + /* translators: %1$s expands to a strong tag, %2$s expands to the product name, %3$s expands to a closing strong tag, %4$s expands to an a tag. %5$s expands to MyYoast, %6$s expands to a closing a tag, %7$s expands to the product name */
532 + __( '%1$s %2$s isn\'t working as expected %3$s and you are not receiving updates or support! Make sure to %4$s activate your product subscription in %5$s%6$s to unlock all the features of %7$s.', 'wordpress-seo' ),
533 + '<strong>',
470 534 $product_name,
471 - '<a href="' . WPSEO_Shortlinker::get( 'https://yoa.st/13j' ) . '" target="_blank">My Yoast</a>'
535 + '</strong>',
536 + '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
537 + 'MyYoast',
538 + '</a>',
539 + $product_name,
472 540 ),
473 - $notification_options
541 + $notification_options,
474 542 );
475 543 }
476 544
477 545 /**
@@ -487,19 +555,22 @@
487 555
488 556 /**
489 557 * Converts a subscription to plugin based format.
490 558 *
491 - * @param stdClass $subscription The subscription to convert.
492 - * @param stdClass $yoast_free_data The Yoast Free's data.
493 - * @param bool $plugin_info Whether we're in the plugin information modal.
494 - * @param string $plugin_file The plugin filename.
559 + * @param stdClass $subscription The subscription to convert.
560 + * @param stdClass|null $yoast_free_data The Yoast Free's data.
561 + * @param bool $plugin_info Whether we're in the plugin information modal.
562 + * @param string $plugin_file The plugin filename.
495 563 *
496 564 * @return stdClass The converted subscription.
497 565 */
498 566 protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
499 - // We need to replace h2's and h3's with h4's because the styling expects that.
500 - $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
501 - $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
567 + $changelog = '';
568 + if ( isset( $subscription->product->changelog ) ) {
569 + // We need to replace h2's and h3's with h4's because the styling expects that.
570 + $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
571 + $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
572 + }
502 573
503 574 // If we're running this because we want to just show the plugin info in the version details modal, we can fallback to the Yoast Free constants, since that modal will not be accessible anyway in the event that the new Free version increases those constants.
504 575 $defaults = [
505 576 // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
@@ -506,9 +577,9 @@
506 577 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
507 578 ];
508 579
509 580 return (object) [
510 - 'new_version' => $subscription->product->version,
581 + 'new_version' => ( $subscription->product->version ?? '' ),
511 582 'name' => $subscription->product->name,
512 583 'slug' => $subscription->product->slug,
513 584 'plugin' => $plugin_file,
514 585 'url' => $subscription->product->store_url,
@@ -526,9 +597,9 @@
526 597 'update_supported' => true,
527 598 'banners' => $this->get_banners( $subscription->product->slug ),
528 599 // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
529 600 'tested' => YOAST_SEO_WP_TESTED,
530 - 'requires' => isset( $yoast_free_data->requires ) ? $yoast_free_data->requires : $defaults['requires'],
601 + 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
531 602 'requires_php' => YOAST_SEO_PHP_REQUIRED,
532 603 ];
533 604 }
534 605
@@ -670,12 +741,17 @@
670 741 protected function get_site_information_transient() {
671 742 global $pagenow;
672 743
673 744 // Force re-check on license & dashboard pages.
674 - $current_page = $this->get_current_page();
745 + $current_page = null;
746 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
747 + if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) {
748 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize.
749 + $current_page = wp_unslash( $_GET['page'] );
750 + }
675 751
676 752 // Check whether the licenses are valid or whether we need to show notifications.
677 - $quick = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
753 + $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE );
678 754
679 755 // Also do a fresh request on Plugins & Core Update pages.
680 756 $quick = $quick || $pagenow === 'plugins.php';
681 757 $quick = $quick || $pagenow === 'update-core.php';
@@ -687,19 +763,8 @@
687 763 return get_transient( self::SITE_INFORMATION_TRANSIENT );
688 764 }
689 765
690 766 /**
691 - * Returns the current page.
692 - *
693 - * @codeCoverageIgnore
694 - *
695 - * @return string The current page.
696 - */
697 - protected function get_current_page() {
698 - return filter_input( INPUT_GET, 'page' );
699 - }
700 -
701 - /**
702 767 * Sets the site information transient.
703 768 *
704 769 * @codeCoverageIgnore
705 770 *
@@ -786,9 +851,9 @@
786 851 'slug' => $subscription->product->slug,
787 852 'last_updated' => $subscription->product->lastUpdated,
788 853 'store_url' => $subscription->product->storeUrl,
789 854 // Ternary operator is necessary because download can be undefined.
790 - 'download' => isset( $subscription->product->download ) ? $subscription->product->download : null,
855 + 'download' => ( $subscription->product->download ?? null ),
791 856 'changelog' => $subscription->product->changelog,
792 857 ],
793 858 ];
794 859 // phpcs:enable