PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | inc/class-addon-manager.php +129 -56 18.0 → trunk 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 */
@@ -96,9 +133,17 @@
96 133 public function register_hooks() {
97 134 add_action( 'admin_init', [ $this, 'validate_addons' ], 15 );
98 135 add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
99 136 add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 );
137 + add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 );
138 + }
100 139
140 + /**
141 + * Registers "expired subscription" warnings to the update messages of our addons.
142 + *
143 + * @return void
144 + */
145 + public function register_expired_messages() {
101 146 foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) {
102 147 add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 );
103 148 }
104 149 }
@@ -165,9 +210,9 @@
165 210
166 211 /**
167 212 * Retrieves a list of (subscription) slugs by the active addons.
168 213 *
169 - * @return array The slugs.
214 + * @return array<string, stdClass> The slugs.
170 215 */
171 216 public function get_subscriptions_for_active_addons() {
172 217 $active_addons = array_keys( $this->get_active_addons() );
173 218 $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
@@ -181,9 +226,9 @@
181 226
182 227 /**
183 228 * Retrieves a list of versions for each addon.
184 229 *
185 - * @return array The addon versions.
230 + * @return array<string, string> The addon versions.
186 231 */
187 232 public function get_installed_addons_versions() {
188 233 $addon_versions = [];
189 234 foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
@@ -230,11 +275,9 @@
230 275 *
231 276 * @return stdClass The list of addons activated for this site.
232 277 */
233 278 public function get_myyoast_site_information() {
234 - if ( $this->site_information === null ) {
235 - $this->site_information = $this->get_site_information_transient();
236 - }
279 + $this->site_information ??= $this->get_site_information_transient();
237 280
238 281 if ( $this->site_information ) {
239 282 return $this->site_information;
240 283 }
@@ -271,9 +314,9 @@
271 314
272 315 /**
273 316 * Checks if there are addon updates.
274 317 *
275 - * @param stdClass|mixed $data The current data for update_plugins.
318 + * @param stdClass $data The current data for update_plugins.
276 319 *
277 320 * @return stdClass Extended data for update_plugins.
278 321 */
279 322 public function check_for_updates( $data ) {
@@ -301,9 +344,9 @@
301 344
302 345 // If the add-on's version is the latest, we have to do no further checks.
303 346 if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) {
304 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.
305 - if ( is_null( $plugin_data->requires ) ) {
348 + if ( $plugin_data->requires === null ) {
306 349 continue;
307 350 }
308 351
309 352 if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
@@ -352,15 +395,37 @@
352 395 /**
353 396 * If the plugin is lacking an active subscription, throw a warning.
354 397 *
355 398 * @param array $plugin_data The data for the plugin in this row.
399 + *
400 + * @return void
356 401 */
357 402 public function expired_subscription_warning( $plugin_data ) {
358 403 $subscription = $this->get_subscription( $plugin_data['slug'] );
359 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 + }
360 416 echo '<br><br>';
361 - /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
362 - 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>';
363 428 }
364 429 }
365 430
366 431 /**
@@ -398,8 +463,10 @@
398 463 }
399 464
400 465 /**
401 466 * Validates the addons and show a notice for the ones that are invalid.
467 + *
468 + * @return void
402 469 */
403 470 public function validate_addons() {
404 471 $notification_center = Yoast_Notification_Center::get();
405 472
@@ -406,19 +473,11 @@
406 473 if ( $notification_center === null ) {
407 474 return;
408 475 }
409 476
410 - $addons = [
411 - 'Yoast SEO Premium' => static::PREMIUM_SLUG,
412 - 'News SEO' => static::NEWS_SLUG,
413 - 'Yoast WooCommerce SEO' => static::WOOCOMMERCE_SLUG,
414 - 'Video SEO' => static::VIDEO_SLUG,
415 - 'Local SEO' => static::LOCAL_SLUG,
416 - ];
477 + foreach ( $this->addon_details as $slug => $addon_info ) {
478 + $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
417 479
418 - foreach ( $addons as $product_name => $slug ) {
419 - $notification = $this->create_notification( $product_name );
420 -
421 480 // Add a notification when the installed plugin isn't activated in My Yoast.
422 481 if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
423 482 $notification_center->add_notification( $notification );
424 483
@@ -429,8 +488,19 @@
429 488 }
430 489 }
431 490
432 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 + /**
433 503 * Removes the site information transients.
434 504 *
435 505 * @codeCoverageIgnore
436 506 *
@@ -444,12 +514,13 @@
444 514 /**
445 515 * Creates an instance of Yoast_Notification.
446 516 *
447 517 * @param string $product_name The product to create the notification for.
518 + * @param string $short_link The short link for the addon notification.
448 519 *
449 520 * @return Yoast_Notification The created notification.
450 521 */
451 - protected function create_notification( $product_name ) {
522 + protected function create_notification( $product_name, $short_link ) {
452 523 $notification_options = [
453 524 'type' => Yoast_Notification::ERROR,
454 525 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
455 526 'capabilities' => 'wpseo_manage_options',
@@ -456,14 +527,19 @@
456 527 ];
457 528
458 529 return new Yoast_Notification(
459 530 sprintf(
460 - /* translators: %1$s expands to the product name. %2$s expands to a link to My Yoast */
461 - __( '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>',
462 534 $product_name,
463 - '<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,
464 540 ),
465 - $notification_options
541 + $notification_options,
466 542 );
467 543 }
468 544
469 545 /**
@@ -479,19 +555,22 @@
479 555
480 556 /**
481 557 * Converts a subscription to plugin based format.
482 558 *
483 - * @param stdClass $subscription The subscription to convert.
484 - * @param stdClass $yoast_free_data The Yoast Free's data.
485 - * @param bool $plugin_info Whether we're in the plugin information modal.
486 - * @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.
487 563 *
488 564 * @return stdClass The converted subscription.
489 565 */
490 566 protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
491 - // We need to replace h2's and h3's with h4's because the styling expects that.
492 - $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
493 - $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 + }
494 573
495 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.
496 575 $defaults = [
497 576 // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
@@ -498,9 +577,9 @@
498 577 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
499 578 ];
500 579
501 580 return (object) [
502 - 'new_version' => $subscription->product->version,
581 + 'new_version' => ( $subscription->product->version ?? '' ),
503 582 'name' => $subscription->product->name,
504 583 'slug' => $subscription->product->slug,
505 584 'plugin' => $plugin_file,
506 585 'url' => $subscription->product->store_url,
@@ -518,9 +597,9 @@
518 597 'update_supported' => true,
519 598 'banners' => $this->get_banners( $subscription->product->slug ),
520 599 // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
521 600 'tested' => YOAST_SEO_WP_TESTED,
522 - 'requires' => isset( $yoast_free_data->requires ) ? $yoast_free_data->requires : $defaults['requires'],
601 + 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
523 602 'requires_php' => YOAST_SEO_PHP_REQUIRED,
524 603 ];
525 604 }
526 605
@@ -662,12 +741,17 @@
662 741 protected function get_site_information_transient() {
663 742 global $pagenow;
664 743
665 744 // Force re-check on license & dashboard pages.
666 - $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 + }
667 751
668 752 // Check whether the licenses are valid or whether we need to show notifications.
669 - $quick = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
753 + $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE );
670 754
671 755 // Also do a fresh request on Plugins & Core Update pages.
672 756 $quick = $quick || $pagenow === 'plugins.php';
673 757 $quick = $quick || $pagenow === 'update-core.php';
@@ -679,19 +763,8 @@
679 763 return get_transient( self::SITE_INFORMATION_TRANSIENT );
680 764 }
681 765
682 766 /**
683 - * Returns the current page.
684 - *
685 - * @codeCoverageIgnore
686 - *
687 - * @return string The current page.
688 - */
689 - protected function get_current_page() {
690 - return filter_input( INPUT_GET, 'page' );
691 - }
692 -
693 - /**
694 767 * Sets the site information transient.
695 768 *
696 769 * @codeCoverageIgnore
697 770 *
@@ -778,9 +851,9 @@
778 851 'slug' => $subscription->product->slug,
779 852 'last_updated' => $subscription->product->lastUpdated,
780 853 'store_url' => $subscription->product->storeUrl,
781 854 // Ternary operator is necessary because download can be undefined.
782 - 'download' => isset( $subscription->product->download ) ? $subscription->product->download : null,
855 + 'download' => ( $subscription->product->download ?? null ),
783 856 'changelog' => $subscription->product->changelog,
784 857 ],
785 858 ];
786 859 // phpcs:enable