PluginProbe ʕ •ᴥ•ʔ
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 24.8.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v24.8.1
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 19.1 19.10 19.11 19.12 19.13 19.14 19.2 19.3 19.4 19.5 19.5.1 19.6 19.6.1 19.7 19.7.1 19.7.2 19.8 19.9 20.0 20.1 20.10 20.11 20.12 20.13 20.2 20.2.1 20.3 20.4 20.5 20.6 20.7 20.8 20.9 21.0 21.1 21.2 21.3 21.4 21.5 21.6 21.7 21.8 21.8.1 21.9 21.9.1 22.0 22.1 22.2 22.3 22.4 22.5 22.6 22.7 22.8 22.9 23.0 23.1 23.2 23.3 23.4 23.5 23.6 23.7 23.8 23.9 24.0 24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8 24.8.1 24.9 25.0 25.1 25.2 25.3 25.3.1 25.4 25.5 25.6 25.7 25.8 25.9 26.0 26.1 26.1.1 26.2 26.3 26.4 26.5 26.6 26.7 26.8 26.9 27.0 27.1 27.1.1 27.2 27.3 27.4
wordpress-seo / inc / class-addon-manager.php
wordpress-seo / inc Last commit date
exceptions 5 years ago options 1 year ago sitemaps 1 year ago class-addon-manager.php 1 year ago class-my-yoast-api-request.php 1 year ago class-post-type.php 1 year ago class-rewrite.php 1 year ago class-upgrade-history.php 1 year ago class-upgrade.php 1 year ago class-wpseo-admin-bar-menu.php 1 year ago class-wpseo-content-images.php 1 year ago class-wpseo-custom-fields.php 1 year ago class-wpseo-custom-taxonomies.php 1 year ago class-wpseo-image-utils.php 1 year ago class-wpseo-installation.php 2 years ago class-wpseo-meta.php 1 year ago class-wpseo-primary-term.php 2 years ago class-wpseo-rank.php 1 year ago class-wpseo-replace-vars.php 1 year ago class-wpseo-replacement-variable.php 5 years ago class-wpseo-shortlinker.php 2 years ago class-wpseo-statistics.php 5 years ago class-wpseo-utils.php 1 year ago class-yoast-dynamic-rewrites.php 2 years ago date-helper.php 5 years ago index.php 10 years ago interface-wpseo-wordpress-ajax-integration.php 7 years ago interface-wpseo-wordpress-integration.php 7 years ago language-utils.php 2 years ago wpseo-functions-deprecated.php 2 years ago wpseo-functions.php 2 years ago wpseo-non-ajax-functions.php 5 years ago
class-addon-manager.php
879 lines
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Inc
6 */
7
8 use Yoast\WP\SEO\Promotions\Application\Promotion_Manager;
9
10 /**
11 * Represents the addon manager.
12 */
13 class WPSEO_Addon_Manager {
14
15 /**
16 * Holds the name of the transient.
17 *
18 * @var string
19 */
20 public const SITE_INFORMATION_TRANSIENT = 'wpseo_site_information';
21
22 /**
23 * Holds the name of the transient.
24 *
25 * @var string
26 */
27 public const SITE_INFORMATION_TRANSIENT_QUICK = 'wpseo_site_information_quick';
28
29 /**
30 * Holds the slug for YoastSEO free.
31 *
32 * @var string
33 */
34 public const FREE_SLUG = 'yoast-seo-wordpress';
35
36 /**
37 * Holds the slug for YoastSEO Premium.
38 *
39 * @var string
40 */
41 public const PREMIUM_SLUG = 'yoast-seo-wordpress-premium';
42
43 /**
44 * Holds the slug for Yoast News.
45 *
46 * @var string
47 */
48 public const NEWS_SLUG = 'yoast-seo-news';
49
50 /**
51 * Holds the slug for Video.
52 *
53 * @var string
54 */
55 public const VIDEO_SLUG = 'yoast-seo-video';
56
57 /**
58 * Holds the slug for WooCommerce.
59 *
60 * @var string
61 */
62 public const WOOCOMMERCE_SLUG = 'yoast-seo-woocommerce';
63
64 /**
65 * Holds the slug for Local.
66 *
67 * @var string
68 */
69 public const LOCAL_SLUG = 'yoast-seo-local';
70
71 /**
72 * The expected addon data.
73 *
74 * @var array
75 */
76 protected static $addons = [
77 'wp-seo-premium.php' => self::PREMIUM_SLUG,
78 'wpseo-news.php' => self::NEWS_SLUG,
79 'video-seo.php' => self::VIDEO_SLUG,
80 'wpseo-woocommerce.php' => self::WOOCOMMERCE_SLUG,
81 'local-seo.php' => self::LOCAL_SLUG,
82 ];
83
84 /**
85 * The addon data for the shortlinks.
86 *
87 * @var array
88 */
89 private $addon_details = [
90 self::PREMIUM_SLUG => [
91 'name' => 'Yoast SEO Premium',
92 'short_link_activation' => 'https://yoa.st/13j',
93 'short_link_renewal' => 'https://yoa.st/4ey',
94 ],
95 self::NEWS_SLUG => [
96 'name' => 'Yoast News SEO',
97 'short_link_activation' => 'https://yoa.st/4xq',
98 'short_link_renewal' => 'https://yoa.st/4xv',
99 ],
100 self::WOOCOMMERCE_SLUG => [
101 'name' => 'Yoast WooCommerce SEO',
102 'short_link_activation' => 'https://yoa.st/4xs',
103 'short_link_renewal' => 'https://yoa.st/4xx',
104 ],
105 self::VIDEO_SLUG => [
106 'name' => 'Yoast Video SEO',
107 'short_link_activation' => 'https://yoa.st/4xr',
108 'short_link_renewal' => 'https://yoa.st/4xw',
109 ],
110 self::LOCAL_SLUG => [
111 'name' => 'Yoast Local SEO',
112 'short_link_activation' => 'https://yoa.st/4xp',
113 'short_link_renewal' => 'https://yoa.st/4xu',
114 ],
115 ];
116
117 /**
118 * Holds the site information data.
119 *
120 * @var stdClass
121 */
122 private $site_information;
123
124 /**
125 * Hooks into WordPress.
126 *
127 * @codeCoverageIgnore
128 *
129 * @return void
130 */
131 public function register_hooks() {
132 add_action( 'admin_init', [ $this, 'validate_addons' ], 15 );
133 add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] );
134 add_filter( 'plugins_api', [ $this, 'get_plugin_information' ], 10, 3 );
135 add_action( 'plugins_loaded', [ $this, 'register_expired_messages' ], 10 );
136 }
137
138 /**
139 * Registers "expired subscription" warnings to the update messages of our addons.
140 *
141 * @return void
142 */
143 public function register_expired_messages() {
144 foreach ( array_keys( $this->get_installed_addons() ) as $plugin_file ) {
145 add_action( 'in_plugin_update_message-' . $plugin_file, [ $this, 'expired_subscription_warning' ], 10, 2 );
146 }
147 }
148
149 /**
150 * Gets the subscriptions for current site.
151 *
152 * @return stdClass The subscriptions.
153 */
154 public function get_subscriptions() {
155 return $this->get_site_information()->subscriptions;
156 }
157
158 /**
159 * Provides a list of addon filenames.
160 *
161 * @return string[] List of addon filenames with their slugs.
162 */
163 public function get_addon_filenames() {
164 return self::$addons;
165 }
166
167 /**
168 * Finds the plugin file.
169 *
170 * @param string $plugin_slug The plugin slug to search.
171 *
172 * @return bool|string Plugin file when installed, False when plugin isn't installed.
173 */
174 public function get_plugin_file( $plugin_slug ) {
175 $plugins = $this->get_plugins();
176 $plugin_files = array_keys( $plugins );
177 $target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true );
178
179 if ( ! $target_plugin_file ) {
180 return false;
181 }
182
183 foreach ( $plugin_files as $plugin_file ) {
184 if ( strpos( $plugin_file, $target_plugin_file ) !== false ) {
185 return $plugin_file;
186 }
187 }
188
189 return false;
190 }
191
192 /**
193 * Retrieves the subscription for the given slug.
194 *
195 * @param string $slug The plugin slug to retrieve.
196 *
197 * @return stdClass|false Subscription data when found, false when not found.
198 */
199 public function get_subscription( $slug ) {
200 foreach ( $this->get_subscriptions() as $subscription ) {
201 if ( $subscription->product->slug === $slug ) {
202 return $subscription;
203 }
204 }
205
206 return false;
207 }
208
209 /**
210 * Retrieves a list of (subscription) slugs by the active addons.
211 *
212 * @return array The slugs.
213 */
214 public function get_subscriptions_for_active_addons() {
215 $active_addons = array_keys( $this->get_active_addons() );
216 $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
217 $subscriptions = [];
218 foreach ( $subscription_slugs as $subscription_slug ) {
219 $subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug );
220 }
221
222 return $subscriptions;
223 }
224
225 /**
226 * Retrieves a list of versions for each addon.
227 *
228 * @return array The addon versions.
229 */
230 public function get_installed_addons_versions() {
231 $addon_versions = [];
232 foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
233 $addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version'];
234 }
235
236 return $addon_versions;
237 }
238
239 /**
240 * Retrieves the plugin information from the subscriptions.
241 *
242 * @param stdClass|false $data The result object. Default false.
243 * @param string $action The type of information being requested from the Plugin Installation API.
244 * @param stdClass $args Plugin API arguments.
245 *
246 * @return object Extended plugin data.
247 */
248 public function get_plugin_information( $data, $action, $args ) {
249 if ( $action !== 'plugin_information' ) {
250 return $data;
251 }
252
253 if ( ! isset( $args->slug ) ) {
254 return $data;
255 }
256
257 $subscription = $this->get_subscription( $args->slug );
258 if ( ! $subscription ) {
259 return $data;
260 }
261
262 $data = $this->convert_subscription_to_plugin( $subscription, null, true );
263
264 if ( $this->has_subscription_expired( $subscription ) ) {
265 unset( $data->package, $data->download_link );
266 }
267
268 return $data;
269 }
270
271 /**
272 * Retrieves information from MyYoast about which addons are connected to the current site.
273 *
274 * @return stdClass The list of addons activated for this site.
275 */
276 public function get_myyoast_site_information() {
277 if ( $this->site_information === null ) {
278 $this->site_information = $this->get_site_information_transient();
279 }
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|mixed $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 ( is_null( $plugin_data->requires ) ) {
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-2023-promotion' ) ) {
409 $sale_copy = sprintf(
410 /* translators: %1$s is a <br> tag. */
411 esc_html__( '%1$s Now with 30%% Black Friday Discount!', 'wordpress-seo' ),
412 '<br>'
413 );
414 }
415 echo '<br><br>';
416 echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> '
417 . sprintf(
418 /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
419 esc_html__( '%1$s can\'t be updated because your product subscription is expired. %2$sRenew your product subscription%3$s to get updates again and use all the features of %1$s.', 'wordpress-seo' ),
420 esc_html( $plugin_data['name'] ),
421 '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">',
422 '</a>'
423 )
424 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
425 . $sale_copy
426 . '</strong>';
427 }
428 }
429
430 /**
431 * Checks if there are any installed addons.
432 *
433 * @return bool True when there are installed Yoast addons.
434 */
435 public function has_installed_addons() {
436 $installed_addons = $this->get_installed_addons();
437
438 return ! empty( $installed_addons );
439 }
440
441 /**
442 * Checks if the plugin is installed and activated in WordPress.
443 *
444 * @param string $slug The class' slug.
445 *
446 * @return bool True when installed and activated.
447 */
448 public function is_installed( $slug ) {
449 $slug_to_class_map = [
450 static::PREMIUM_SLUG => 'WPSEO_Premium',
451 static::NEWS_SLUG => 'WPSEO_News',
452 static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
453 static::VIDEO_SLUG => 'WPSEO_Video_Sitemap',
454 static::LOCAL_SLUG => 'WPSEO_Local_Core',
455 ];
456
457 if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
458 return false;
459 }
460
461 return class_exists( $slug_to_class_map[ $slug ] );
462 }
463
464 /**
465 * Validates the addons and show a notice for the ones that are invalid.
466 *
467 * @return void
468 */
469 public function validate_addons() {
470 $notification_center = Yoast_Notification_Center::get();
471
472 if ( $notification_center === null ) {
473 return;
474 }
475
476 foreach ( $this->addon_details as $slug => $addon_info ) {
477 $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
478
479 // Add a notification when the installed plugin isn't activated in My Yoast.
480 if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
481 $notification_center->add_notification( $notification );
482
483 continue;
484 }
485
486 $notification_center->remove_notification( $notification );
487 }
488 }
489
490 /**
491 * Removes the site information transients.
492 *
493 * @codeCoverageIgnore
494 *
495 * @return void
496 */
497 public function remove_site_information_transients() {
498 delete_transient( self::SITE_INFORMATION_TRANSIENT );
499 delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
500 }
501
502 /**
503 * Creates an instance of Yoast_Notification.
504 *
505 * @param string $product_name The product to create the notification for.
506 * @param string $short_link The short link for the addon notification.
507 *
508 * @return Yoast_Notification The created notification.
509 */
510 protected function create_notification( $product_name, $short_link ) {
511 $notification_options = [
512 'type' => Yoast_Notification::ERROR,
513 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
514 'capabilities' => 'wpseo_manage_options',
515 ];
516
517 return new Yoast_Notification(
518 sprintf(
519 /* 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 */
520 __( '%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' ),
521 '<strong>',
522 $product_name,
523 '</strong>',
524 '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
525 'MyYoast',
526 '</a>',
527 $product_name
528 ),
529 $notification_options
530 );
531 }
532
533 /**
534 * Checks whether a plugin expiry date has been passed.
535 *
536 * @param stdClass $subscription Plugin subscription.
537 *
538 * @return bool Has the plugin expired.
539 */
540 protected function has_subscription_expired( $subscription ) {
541 return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
542 }
543
544 /**
545 * Converts a subscription to plugin based format.
546 *
547 * @param stdClass $subscription The subscription to convert.
548 * @param stdClass|null $yoast_free_data The Yoast Free's data.
549 * @param bool $plugin_info Whether we're in the plugin information modal.
550 * @param string $plugin_file The plugin filename.
551 *
552 * @return stdClass The converted subscription.
553 */
554 protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
555 $changelog = '';
556 if ( isset( $subscription->product->changelog ) ) {
557 // We need to replace h2's and h3's with h4's because the styling expects that.
558 $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
559 $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
560 }
561
562 // 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.
563 $defaults = [
564 // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
565 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
566 ];
567
568 return (object) [
569 'new_version' => ( $subscription->product->version ?? '' ),
570 'name' => $subscription->product->name,
571 'slug' => $subscription->product->slug,
572 'plugin' => $plugin_file,
573 'url' => $subscription->product->store_url,
574 'last_update' => $subscription->product->last_updated,
575 'homepage' => $subscription->product->store_url,
576 'download_link' => $subscription->product->download,
577 'package' => $subscription->product->download,
578 'sections' => [
579 'changelog' => $changelog,
580 'support' => $this->get_support_section(),
581 ],
582 'icons' => [
583 '2x' => $this->get_icon( $subscription->product->slug ),
584 ],
585 'update_supported' => true,
586 'banners' => $this->get_banners( $subscription->product->slug ),
587 // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
588 'tested' => YOAST_SEO_WP_TESTED,
589 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
590 'requires_php' => YOAST_SEO_PHP_REQUIRED,
591 ];
592 }
593
594 /**
595 * Returns the plugin's icon URL.
596 *
597 * @param string $slug The plugin slug.
598 *
599 * @return string The icon URL for this plugin.
600 */
601 protected function get_icon( $slug ) {
602 switch ( $slug ) {
603 case self::LOCAL_SLUG:
604 return 'https://yoa.st/local-seo-icon';
605 case self::NEWS_SLUG:
606 return 'https://yoa.st/news-seo-icon';
607 case self::PREMIUM_SLUG:
608 return 'https://yoa.st/yoast-seo-icon';
609 case self::VIDEO_SLUG:
610 return 'https://yoa.st/video-seo-icon';
611 case self::WOOCOMMERCE_SLUG:
612 return 'https://yoa.st/woo-seo-icon';
613 }
614 }
615
616 /**
617 * Return an array of plugin banner URLs.
618 *
619 * @param string $slug The plugin slug.
620 *
621 * @return string[]
622 */
623 protected function get_banners( $slug ) {
624 switch ( $slug ) {
625 case self::LOCAL_SLUG:
626 return [
627 'high' => 'https://yoa.st/yoast-seo-banner-local',
628 'low' => 'https://yoa.st/yoast-seo-banner-low-local',
629 ];
630 case self::NEWS_SLUG:
631 return [
632 'high' => 'https://yoa.st/yoast-seo-banner-news',
633 'low' => 'https://yoa.st/yoast-seo-banner-low-news',
634 ];
635 case self::PREMIUM_SLUG:
636 return [
637 'high' => 'https://yoa.st/yoast-seo-banner-premium',
638 'low' => 'https://yoa.st/yoast-seo-banner-low-premium',
639 ];
640 case self::VIDEO_SLUG:
641 return [
642 'high' => 'https://yoa.st/yoast-seo-banner-video',
643 'low' => 'https://yoa.st/yoast-seo-banner-low-video',
644 ];
645 case self::WOOCOMMERCE_SLUG:
646 return [
647 'high' => 'https://yoa.st/yoast-seo-banner-woo',
648 'low' => 'https://yoa.st/yoast-seo-banner-low-woo',
649 ];
650 }
651 }
652
653 /**
654 * Checks if the given plugin_file belongs to a Yoast addon.
655 *
656 * @param string $plugin_file Path to the plugin.
657 *
658 * @return bool True when plugin file is for a Yoast addon.
659 */
660 protected function is_yoast_addon( $plugin_file ) {
661 return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
662 }
663
664 /**
665 * Retrieves the addon slug by given plugin file path.
666 *
667 * @param string $plugin_file The file path to the plugin.
668 *
669 * @return string The slug when found or empty string when not.
670 */
671 protected function get_slug_by_plugin_file( $plugin_file ) {
672 $addons = self::$addons;
673
674 // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
675 if ( YoastSEO()->helpers->product->is_premium() ) {
676 $addons['wp-seo.php'] = self::FREE_SLUG;
677 }
678
679 foreach ( $addons as $addon => $addon_slug ) {
680 if ( strpos( $plugin_file, $addon ) !== false ) {
681 return $addon_slug;
682 }
683 }
684
685 return '';
686 }
687
688 /**
689 * Retrieves the installed Yoast addons.
690 *
691 * @return array The installed plugins.
692 */
693 protected function get_installed_addons() {
694 return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY );
695 }
696
697 /**
698 * Retrieves a list of active addons.
699 *
700 * @return array The active addons.
701 */
702 protected function get_active_addons() {
703 return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
704 }
705
706 /**
707 * Retrieves the current sites from the API.
708 *
709 * @codeCoverageIgnore
710 *
711 * @return bool|stdClass Object when request is successful. False if not.
712 */
713 protected function request_current_sites() {
714 $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
715 if ( $api_request->fire() ) {
716 return $api_request->get_response();
717 }
718
719 return $this->get_site_information_default();
720 }
721
722 /**
723 * Retrieves the transient value with the site information.
724 *
725 * @codeCoverageIgnore
726 *
727 * @return stdClass|false The transient value.
728 */
729 protected function get_site_information_transient() {
730 global $pagenow;
731
732 // Force re-check on license & dashboard pages.
733 $current_page = null;
734 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
735 if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) {
736 // 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.
737 $current_page = wp_unslash( $_GET['page'] );
738 }
739
740 // Check whether the licenses are valid or whether we need to show notifications.
741 $quick = ( $current_page === 'wpseo_licenses' || $current_page === 'wpseo_dashboard' );
742
743 // Also do a fresh request on Plugins & Core Update pages.
744 $quick = $quick || $pagenow === 'plugins.php';
745 $quick = $quick || $pagenow === 'update-core.php';
746
747 if ( $quick ) {
748 return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
749 }
750
751 return get_transient( self::SITE_INFORMATION_TRANSIENT );
752 }
753
754 /**
755 * Sets the site information transient.
756 *
757 * @codeCoverageIgnore
758 *
759 * @param stdClass $site_information The site information to save.
760 *
761 * @return void
762 */
763 protected function set_site_information_transient( $site_information ) {
764 set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
765 set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 );
766 }
767
768 /**
769 * Retrieves all installed WordPress plugins.
770 *
771 * @codeCoverageIgnore
772 *
773 * @return array The plugins.
774 */
775 protected function get_plugins() {
776 if ( ! function_exists( 'get_plugins' ) ) {
777 require_once ABSPATH . 'wp-admin/includes/plugin.php';
778 }
779
780 return get_plugins();
781 }
782
783 /**
784 * Checks if the given plugin file belongs to an active plugin.
785 *
786 * @codeCoverageIgnore
787 *
788 * @param string $plugin_file The file path to the plugin.
789 *
790 * @return bool True when plugin is active.
791 */
792 protected function is_plugin_active( $plugin_file ) {
793 return is_plugin_active( $plugin_file );
794 }
795
796 /**
797 * Returns an object with no subscriptions.
798 *
799 * @codeCoverageIgnore
800 *
801 * @return stdClass Site information.
802 */
803 protected function get_site_information_default() {
804 return (object) [
805 'url' => WPSEO_Utils::get_home_url(),
806 'subscriptions' => [],
807 ];
808 }
809
810 /**
811 * Maps the plugin API response.
812 *
813 * @param object $site_information Site information as received from the API.
814 *
815 * @return stdClass Mapped site information.
816 */
817 protected function map_site_information( $site_information ) {
818 return (object) [
819 'url' => $site_information->url,
820 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
821 ];
822 }
823
824 /**
825 * Maps a plugin subscription.
826 *
827 * @param object $subscription Subscription information as received from the API.
828 *
829 * @return stdClass Mapped subscription.
830 */
831 protected function map_subscription( $subscription ) {
832 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties.
833 return (object) [
834 'renewal_url' => $subscription->renewalUrl,
835 'expiry_date' => $subscription->expiryDate,
836 'product' => (object) [
837 'version' => $subscription->product->version,
838 'name' => $subscription->product->name,
839 'slug' => $subscription->product->slug,
840 'last_updated' => $subscription->product->lastUpdated,
841 'store_url' => $subscription->product->storeUrl,
842 // Ternary operator is necessary because download can be undefined.
843 'download' => ( $subscription->product->download ?? null ),
844 'changelog' => $subscription->product->changelog,
845 ],
846 ];
847 // phpcs:enable
848 }
849
850 /**
851 * Retrieves the site information.
852 *
853 * @return stdClass The site information.
854 */
855 private function get_site_information() {
856 if ( ! $this->has_installed_addons() ) {
857 return $this->get_site_information_default();
858 }
859
860 return $this->get_myyoast_site_information();
861 }
862
863 /**
864 * Retrieves the contents for the support section.
865 *
866 * @return string The support section content.
867 */
868 protected function get_support_section() {
869 return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>'
870 . '<p>'
871 /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */
872 . 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>' )
873 . ' '
874 /* translators: %s expands to a mailto support link. */
875 . 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>' )
876 . '</p>';
877 }
878 }
879