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-addon-manager.php

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

891 lines 26.5 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\Inc
6 */
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
12 /**
13 * Represents the addon manager.
14 */
15 class WPSEO_Addon_Manager {
16
17 /**
18 * Holds the name of the transient.
19 *
20 * @var string
21 */
22 public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
23
24 /**
25 * Holds the name of the transient.
26 *
27 * @var string
28 */
29 public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick';
30
31 /**
32 * Holds the slug for YoastSEO free.
33 *
34 * @var string
35 */
36 public const FREE_SLUG = 'yoast-seo-wordpress';
37
38 /**
39 * Holds the slug for YoastSEO Premium.
40 *
41 * @var string
42 */
43 public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
44
45 /**
46 * Holds the slug for Yoast News.
47 *
48 * @var string
49 */
50 public const NEWS_SLUG = 'yoast-seo-news';
51
52 /**
53 * Holds the slug for Video.
54 *
55 * @var string
56 */
57 public const VIDEO_SLUG = 'yoast-seo-video';
58
59 /**
60 * Holds the slug for WooCommerce.
61 *
62 * @var string
63 */
64 public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
65
66 /**
67 * Holds the slug for Local.
68 *
69 * @var string
70 */
71 public const LOCAL_SLUG = 'yoast-seo-local';
72
73 /**
74 * The expected addon data.
75 *
76 * @var array<string, string>
77 */
78 protected static $addons = [
79 'wp-seo-premium.php' => self::PREMIUM_SLUG,
80 'wpseo-news.php' => self::NEWS_SLUG,
81 'video-seo.php' => self::VIDEO_SLUG,
82 'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG,
83 'local-seo.php' => self::LOCAL_SLUG,
84 ];
85
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 /**
120 * Holds the site information data.
121 *
122 * @var stdClass
123 */
124 private $site_information;
125
126 /**
127 * Hooks into WordPress.
128 *
129 * @codeCoverageIgnore
130 *
131 * @return void
132 */
133 public function register_hooks() {
134 add_action( 'admin_init', [ $this, 'validate_addons' ], 15 );
135 add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
136 add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 );
137 add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 );
138 }
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() {
146 foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) {
147 add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 );
148 }
149 }
150
151 /**
152 * Gets the subscriptions for current site.
153 *
154 * @return stdClass The subscriptions.
155 */
156 public function get_subscriptions() {
157 return $this->get_site_information()->subscriptions;
158 }
159
160 /**
161 * Provides a list of addon filenames.
162 *
163 * @return string[] List of addon filenames with their slugs.
164 */
165 public function get_addon_filenames() {
166 return self::$addons;
167 }
168
169 /**
170 * Finds the plugin file.
171 *
172 * @param string $plugin_slug The plugin slug to search.
173 *
174 * @return bool|string Plugin file when installed, False when plugin isn't installed.
175 */
176 public function get_plugin_file( $plugin_slug ) {
177 $plugins = $this->get_plugins();
178 $plugin_files = array_keys( $plugins );
179 $target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true );
180
181 if ( ! $target_plugin_file ) {
182 return false;
183 }
184
185 foreach ( $plugin_files as $plugin_file ) {
186 if ( strpos( $plugin_file, $target_plugin_file ) !== false ) {
187 return $plugin_file;
188 }
189 }
190
191 return false;
192 }
193
194 /**
195 * Retrieves the subscription for the given slug.
196 *
197 * @param string $slug The plugin slug to retrieve.
198 *
199 * @return stdClass|false Subscription data when found, false when not found.
200 */
201 public function get_subscription( $slug ) {
202 foreach ( $this->get_subscriptions() as $subscription ) {
203 if ( $subscription->product->slug === $slug ) {
204 return $subscription;
205 }
206 }
207
208 return false;
209 }
210
211 /**
212 * Retrieves a list of (subscription) slugs by the active addons.
213 *
214 * @return array<string, stdClass> The slugs.
215 */
216 public function get_subscriptions_for_active_addons() {
217 $active_addons = array_keys( $this->get_active_addons() );
218 $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
219 $subscriptions = [];
220 foreach ( $subscription_slugs as $subscription_slug ) {
221 $subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug );
222 }
223
224 return $subscriptions;
225 }
226
227 /**
228 * Retrieves a list of versions for each addon.
229 *
230 * @return array<string, string> The addon versions.
231 */
232 public function get_installed_addons_versions() {
233 $addon_versions = [];
234 foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
235 $addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version'];
236 }
237
238 return $addon_versions;
239 }
240
241 /**
242 * Retrieves the plugin information from the subscriptions.
243 *
244 * @param stdClass|false $data The result object. Default false.
245 * @param string $action The type of information being requested from the Plugin Installation API.
246 * @param stdClass $args Plugin API arguments.
247 *
248 * @return object Extended plugin data.
249 */
250 public function get_plugin_information( $data, $action, $args ) {
251 if ( $action !== 'plugin_information' ) {
252 return $data;
253 }
254
255 if ( ! isset( $args->slug ) ) {
256 return $data;
257 }
258
259 $subscription = $this->get_subscription( $args->slug );
260 if ( ! $subscription ) {
261 return $data;
262 }
263
264 $data = $this->convert_subscription_to_plugin( $subscription, null, true );
265
266 if ( $this->has_subscription_expired( $subscription ) ) {
267 unset( $data->package, $data->download_link );
268 }
269
270 return $data;
271 }
272
273 /**
274 * Retrieves information from MyYoast about which addons are connected to the current site.
275 *
276 * @return stdClass The list of addons activated for this site.
277 */
278 public function get_myyoast_site_information() {
279 $this->site_information ??= $this->get_site_information_transient();
280
281 if ( $this->site_information ) {
282 return $this->site_information;
283 }
284
285 $this->site_information = $this->request_current_sites();
286 if ( $this->site_information ) {
287 $this->site_information = $this->map_site_information( $this->site_information );
288
289 $this->set_site_information_transient( $this->site_information );
290
291 return $this->site_information;
292 }
293
294 return $this->get_site_information_default();
295 }
296
297 /**
298 * Checks if the subscription for the given slug is valid.
299 *
300 * @param string $slug The plugin slug to retrieve.
301 *
302 * @return bool True when the subscription is valid.
303 */
304 public function has_valid_subscription( $slug ) {
305 $subscription = $this->get_subscription( $slug );
306
307 // An non-existing subscription is never valid.
308 if ( ! $subscription ) {
309 return false;
310 }
311
312 return ! $this->has_subscription_expired( $subscription );
313 }
314
315 /**
316 * Checks if there are addon updates.
317 *
318 * @param stdClass $data The current data for update_plugins.
319 *
320 * @return stdClass Extended data for update_plugins.
321 */
322 public function check_for_updates( $data ) {
323 global $wp_version;
324
325 if ( empty( $data ) ) {
326 return $data;
327 }
328
329 // We have to figure out if we're safe to upgrade the add-ons, based on what the latest Yoast Free requirements for the WP version is.
330 $yoast_free_data = $this->extract_yoast_data( $data );
331
332 foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) {
333 $subscription_slug = $this->get_slug_by_plugin_file( $plugin_file );
334 $subscription = $this->get_subscription( $subscription_slug );
335
336 if ( ! $subscription ) {
337 continue;
338 }
339
340 $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file );
341
342 // Let's assume for now that it will get added in the 'no_update' key that we'll return to the WP API.
343 $is_no_update = true;
344
345 // If the add-on's version is the latest, we have to do no further checks.
346 if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) {
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.
348 if ( $plugin_data->requires === null ) {
349 continue;
350 }
351
352 if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
353 // The add-on has an available update *and* the Yoast Free requirements for the WP version are also met, so go ahead and show the upgrade info to the user.
354 $is_no_update = false;
355 $data->response[ $plugin_file ] = $plugin_data;
356
357 if ( $this->has_subscription_expired( $subscription ) ) {
358 unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link );
359 }
360 }
361 }
362
363 if ( $is_no_update ) {
364 // Still convert subscription when no updates is available.
365 $data->no_update[ $plugin_file ] = $plugin_data;
366
367 if ( $this->has_subscription_expired( $subscription ) ) {
368 unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link );
369 }
370 }
371 }
372
373 return $data;
374 }
375
376 /**
377 * Extracts Yoast SEO Free's data from the wp.org API response.
378 *
379 * @param object $data The wp.org API response.
380 *
381 * @return object Yoast Free's data from wp.org.
382 */
383 protected function extract_yoast_data( $data ) {
384 if ( isset( $data->response[ WPSEO_BASENAME ] ) ) {
385 return $data->response[ WPSEO_BASENAME ];
386 }
387
388 if ( isset( $data->no_update[ WPSEO_BASENAME ] ) ) {
389 return $data->no_update[ WPSEO_BASENAME ];
390 }
391
392 return (object) [];
393 }
394
395 /**
396 * If the plugin is lacking an active subscription, throw a warning.
397 *
398 * @param array $plugin_data The data for the plugin in this row.
399 *
400 * @return void
401 */
402 public function expired_subscription_warning( $plugin_data ) {
403 $subscription = $this->get_subscription( $plugin_data['slug'] );
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 }
416 echo '<br><br>';
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>';
428 }
429 }
430
431 /**
432 * Checks if there are any installed addons.
433 *
434 * @return bool True when there are installed Yoast addons.
435 */
436 public function has_installed_addons() {
437 $installed_addons = $this->get_installed_addons();
438
439 return ! empty( $installed_addons );
440 }
441
442 /**
443 * Checks if the plugin is installed and activated in WordPress.
444 *
445 * @param string $slug The class' slug.
446 *
447 * @return bool True when installed and activated.
448 */
449 public function is_installed( $slug ) {
450 $slug_to_class_map = [
451 static::PREMIUM_SLUG => 'WPSEO_Premium',
452 static::NEWS_SLUG => 'WPSEO_News',
453 static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
454 static::VIDEO_SLUG => 'WPSEO_Video_Sitemap',
455 static::LOCAL_SLUG => 'WPSEO_Local_Core',
456 ];
457
458 if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
459 return false;
460 }
461
462 return class_exists( $slug_to_class_map[ $slug ] );
463 }
464
465 /**
466 * Validates the addons and show a notice for the ones that are invalid.
467 *
468 * @return void
469 */
470 public function validate_addons() {
471 $notification_center = Yoast_Notification_Center::get();
472
473 if ( $notification_center === null ) {
474 return;
475 }
476
477 foreach ( $this->addon_details as $slug => $addon_info ) {
478 $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
479
480 // Add a notification when the installed plugin isn't activated in My Yoast.
481 if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
482 $notification_center->add_notification( $notification );
483
484 continue;
485 }
486
487 $notification_center->remove_notification( $notification );
488 }
489 }
490
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 /**
503 * Removes the site information transients.
504 *
505 * @codeCoverageIgnore
506 *
507 * @return void
508 */
509 public function remove_site_information_transients() {
510 delete_transient( self::SITE_INFORMATION_TRANSIENT );
511 delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
512 }
513
514 /**
515 * Creates an instance of Yoast_Notification.
516 *
517 * @param string $product_name The product to create the notification for.
518 * @param string $short_link The short link for the addon notification.
519 *
520 * @return Yoast_Notification The created notification.
521 */
522 protected function create_notification( $product_name, $short_link ) {
523 $notification_options = [
524 'type' => Yoast_Notification::ERROR,
525 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
526 'capabilities' => 'wpseo_manage_options',
527 ];
528
529 return new Yoast_Notification(
530 sprintf(
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>',
534 $product_name,
535 '</strong>',
536 '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
537 'MyYoast',
538 '</a>',
539 $product_name,
540 ),
541 $notification_options,
542 );
543 }
544
545 /**
546 * Checks whether a plugin expiry date has been passed.
547 *
548 * @param stdClass $subscription Plugin subscription.
549 *
550 * @return bool Has the plugin expired.
551 */
552 protected function has_subscription_expired( $subscription ) {
553 return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
554 }
555
556 /**
557 * Converts a subscription to plugin based format.
558 *
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.
563 *
564 * @return stdClass The converted subscription.
565 */
566 protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
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 }
573
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.
575 $defaults = [
576 // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
577 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
578 ];
579
580 return (object) [
581 'new_version' => ( $subscription->product->version ?? '' ),
582 'name' => $subscription->product->name,
583 'slug' => $subscription->product->slug,
584 'plugin' => $plugin_file,
585 'url' => $subscription->product->store_url,
586 'last_update' => $subscription->product->last_updated,
587 'homepage' => $subscription->product->store_url,
588 'download_link' => $subscription->product->download,
589 'package' => $subscription->product->download,
590 'sections' => [
591 'changelog' => $changelog,
592 'support' => $this->get_support_section(),
593 ],
594 'icons' => [
595 '2x' => $this->get_icon( $subscription->product->slug ),
596 ],
597 'update_supported' => true,
598 'banners' => $this->get_banners( $subscription->product->slug ),
599 // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
600 'tested' => YOAST_SEO_WP_TESTED,
601 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
602 'requires_php' => YOAST_SEO_PHP_REQUIRED,
603 ];
604 }
605
606 /**
607 * Returns the plugin's icon URL.
608 *
609 * @param string $slug The plugin slug.
610 *
611 * @return string The icon URL for this plugin.
612 */
613 protected function get_icon( $slug ) {
614 switch ( $slug ) {
615 case self::LOCAL_SLUG:
616 return 'https://yoa.st/local-seo-icon';
617 case self::NEWS_SLUG:
618 return 'https://yoa.st/news-seo-icon';
619 case self::PREMIUM_SLUG:
620 return 'https://yoa.st/yoast-seo-icon';
621 case self::VIDEO_SLUG:
622 return 'https://yoa.st/video-seo-icon';
623 case self::WOOCOMMERCE_SLUG:
624 return 'https://yoa.st/woo-seo-icon';
625 }
626 }
627
628 /**
629 * Return an array of plugin banner URLs.
630 *
631 * @param string $slug The plugin slug.
632 *
633 * @return string[]
634 */
635 protected function get_banners( $slug ) {
636 switch ( $slug ) {
637 case self::LOCAL_SLUG:
638 return [
639 'high' => 'https://yoa.st/yoast-seo-banner-local',
640 'low' => 'https://yoa.st/yoast-seo-banner-low-local',
641 ];
642 case self::NEWS_SLUG:
643 return [
644 'high' => 'https://yoa.st/yoast-seo-banner-news',
645 'low' => 'https://yoa.st/yoast-seo-banner-low-news',
646 ];
647 case self::PREMIUM_SLUG:
648 return [
649 'high' => 'https://yoa.st/yoast-seo-banner-premium',
650 'low' => 'https://yoa.st/yoast-seo-banner-low-premium',
651 ];
652 case self::VIDEO_SLUG:
653 return [
654 'high' => 'https://yoa.st/yoast-seo-banner-video',
655 'low' => 'https://yoa.st/yoast-seo-banner-low-video',
656 ];
657 case self::WOOCOMMERCE_SLUG:
658 return [
659 'high' => 'https://yoa.st/yoast-seo-banner-woo',
660 'low' => 'https://yoa.st/yoast-seo-banner-low-woo',
661 ];
662 }
663 }
664
665 /**
666 * Checks if the given plugin_file belongs to a Yoast addon.
667 *
668 * @param string $plugin_file Path to the plugin.
669 *
670 * @return bool True when plugin file is for a Yoast addon.
671 */
672 protected function is_yoast_addon( $plugin_file ) {
673 return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
674 }
675
676 /**
677 * Retrieves the addon slug by given plugin file path.
678 *
679 * @param string $plugin_file The file path to the plugin.
680 *
681 * @return string The slug when found or empty string when not.
682 */
683 protected function get_slug_by_plugin_file( $plugin_file ) {
684 $addons = self::$addons;
685
686 // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
687 if ( YoastSEO()->helpers->product->is_premium() ) {
688 $addons['wp-seo.php'] = self::FREE_SLUG;
689 }
690
691 foreach ( $addons as $addon => $addon_slug ) {
692 if ( strpos( $plugin_file, $addon ) !== false ) {
693 return $addon_slug;
694 }
695 }
696
697 return '';
698 }
699
700 /**
701 * Retrieves the installed Yoast addons.
702 *
703 * @return array The installed plugins.
704 */
705 protected function get_installed_addons() {
706 return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY );
707 }
708
709 /**
710 * Retrieves a list of active addons.
711 *
712 * @return array The active addons.
713 */
714 protected function get_active_addons() {
715 return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
716 }
717
718 /**
719 * Retrieves the current sites from the API.
720 *
721 * @codeCoverageIgnore
722 *
723 * @return bool|stdClass Object when request is successful. False if not.
724 */
725 protected function request_current_sites() {
726 $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
727 if ( $api_request->fire() ) {
728 return $api_request->get_response();
729 }
730
731 return $this->get_site_information_default();
732 }
733
734 /**
735 * Retrieves the transient value with the site information.
736 *
737 * @codeCoverageIgnore
738 *
739 * @return stdClass|false The transient value.
740 */
741 protected function get_site_information_transient() {
742 global $pagenow;
743
744 // Force re-check on license & dashboard pages.
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 }
751
752 // Check whether the licenses are valid or whether we need to show notifications.
753 $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE );
754
755 // Also do a fresh request on Plugins & Core Update pages.
756 $quick = $quick || $pagenow === 'plugins.php';
757 $quick = $quick || $pagenow === 'update-core.php';
758
759 if ( $quick ) {
760 return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
761 }
762
763 return get_transient( self::SITE_INFORMATION_TRANSIENT );
764 }
765
766 /**
767 * Sets the site information transient.
768 *
769 * @codeCoverageIgnore
770 *
771 * @param stdClass $site_information The site information to save.
772 *
773 * @return void
774 */
775 protected function set_site_information_transient( $site_information ) {
776 set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
777 set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 );
778 }
779
780 /**
781 * Retrieves all installed WordPress plugins.
782 *
783 * @codeCoverageIgnore
784 *
785 * @return array The plugins.
786 */
787 protected function get_plugins() {
788 if ( ! function_exists( 'get_plugins' ) ) {
789 require_once ABSPATH . 'wp-admin/includes/plugin.php';
790 }
791
792 return get_plugins();
793 }
794
795 /**
796 * Checks if the given plugin file belongs to an active plugin.
797 *
798 * @codeCoverageIgnore
799 *
800 * @param string $plugin_file The file path to the plugin.
801 *
802 * @return bool True when plugin is active.
803 */
804 protected function is_plugin_active( $plugin_file ) {
805 return is_plugin_active( $plugin_file );
806 }
807
808 /**
809 * Returns an object with no subscriptions.
810 *
811 * @codeCoverageIgnore
812 *
813 * @return stdClass Site information.
814 */
815 protected function get_site_information_default() {
816 return (object) [
817 'url' => WPSEO_Utils::get_home_url(),
818 'subscriptions' => [],
819 ];
820 }
821
822 /**
823 * Maps the plugin API response.
824 *
825 * @param object $site_information Site information as received from the API.
826 *
827 * @return stdClass Mapped site information.
828 */
829 protected function map_site_information( $site_information ) {
830 return (object) [
831 'url' => $site_information->url,
832 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
833 ];
834 }
835
836 /**
837 * Maps a plugin subscription.
838 *
839 * @param object $subscription Subscription information as received from the API.
840 *
841 * @return stdClass Mapped subscription.
842 */
843 protected function map_subscription( $subscription ) {
844 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties.
845 return (object) [
846 'renewal_url' => $subscription->renewalUrl,
847 'expiry_date' => $subscription->expiryDate,
848 'product' => (object) [
849 'version' => $subscription->product->version,
850 'name' => $subscription->product->name,
851 'slug' => $subscription->product->slug,
852 'last_updated' => $subscription->product->lastUpdated,
853 'store_url' => $subscription->product->storeUrl,
854 // Ternary operator is necessary because download can be undefined.
855 'download' => ( $subscription->product->download ?? null ),
856 'changelog' => $subscription->product->changelog,
857 ],
858 ];
859 // phpcs:enable
860 }
861
862 /**
863 * Retrieves the site information.
864 *
865 * @return stdClass The site information.
866 */
867 private function get_site_information() {
868 if ( ! $this->has_installed_addons() ) {
869 return $this->get_site_information_default();
870 }
871
872 return $this->get_myyoast_site_information();
873 }
874
875 /**
876 * Retrieves the contents for the support section.
877 *
878 * @return string The support section content.
879 */
880 protected function get_support_section() {
881 return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>'
882 . '<p>'
883 /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */
884 . sprintf( __( 'You can probably find an answer to your question in our %1$shelp center%2$s.', 'wordpress-seo' ), '<a href="https://yoast.com/help/">', '</a>' )
885 . ' '
886 /* translators: %s expands to a mailto support link. */
887 . sprintf( __( 'If you still need support and have an active subscription for this product, please email %s.', 'wordpress-seo' ), '<a href="mailto:support@yoast.com">support@yoast.com</a>' )
888 . '</p>';
889 }
890 }
891