PluginProbe ʕ •ᴥ•ʔ
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 25.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v25.5
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 11 months 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 11 months 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
881 lines
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
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
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 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 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 if ( $this->site_information === null ) {
280 $this->site_information = $this->get_site_information_transient();
281 }
282
283 if ( $this->site_information ) {
284 return $this->site_information;
285 }
286
287 $this->site_information = $this->request_current_sites();
288 if ( $this->site_information ) {
289 $this->site_information = $this->map_site_information( $this->site_information );
290
291 $this->set_site_information_transient( $this->site_information );
292
293 return $this->site_information;
294 }
295
296 return $this->get_site_information_default();
297 }
298
299 /**
300 * Checks if the subscription for the given slug is valid.
301 *
302 * @param string $slug The plugin slug to retrieve.
303 *
304 * @return bool True when the subscription is valid.
305 */
306 public function has_valid_subscription( $slug ) {
307 $subscription = $this->get_subscription( $slug );
308
309 // An non-existing subscription is never valid.
310 if ( ! $subscription ) {
311 return false;
312 }
313
314 return ! $this->has_subscription_expired( $subscription );
315 }
316
317 /**
318 * Checks if there are addon updates.
319 *
320 * @param stdClass|mixed $data The current data for update_plugins.
321 *
322 * @return stdClass Extended data for update_plugins.
323 */
324 public function check_for_updates( $data ) {
325 global $wp_version;
326
327 if ( empty( $data ) ) {
328 return $data;
329 }
330
331 // 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.
332 $yoast_free_data = $this->extract_yoast_data( $data );
333
334 foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) {
335 $subscription_slug = $this->get_slug_by_plugin_file( $plugin_file );
336 $subscription = $this->get_subscription( $subscription_slug );
337
338 if ( ! $subscription ) {
339 continue;
340 }
341
342 $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file );
343
344 // Let's assume for now that it will get added in the 'no_update' key that we'll return to the WP API.
345 $is_no_update = true;
346
347 // If the add-on's version is the latest, we have to do no further checks.
348 if ( version_compare( $installed_plugin['Version'], $plugin_data->new_version, '<' ) ) {
349 // 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.
350 if ( $plugin_data->requires === null ) {
351 continue;
352 }
353
354 if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
355 // 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.
356 $is_no_update = false;
357 $data->response[ $plugin_file ] = $plugin_data;
358
359 if ( $this->has_subscription_expired( $subscription ) ) {
360 unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link );
361 }
362 }
363 }
364
365 if ( $is_no_update ) {
366 // Still convert subscription when no updates is available.
367 $data->no_update[ $plugin_file ] = $plugin_data;
368
369 if ( $this->has_subscription_expired( $subscription ) ) {
370 unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link );
371 }
372 }
373 }
374
375 return $data;
376 }
377
378 /**
379 * Extracts Yoast SEO Free's data from the wp.org API response.
380 *
381 * @param object $data The wp.org API response.
382 *
383 * @return object Yoast Free's data from wp.org.
384 */
385 protected function extract_yoast_data( $data ) {
386 if ( isset( $data->response[ WPSEO_BASENAME ] ) ) {
387 return $data->response[ WPSEO_BASENAME ];
388 }
389
390 if ( isset( $data->no_update[ WPSEO_BASENAME ] ) ) {
391 return $data->no_update[ WPSEO_BASENAME ];
392 }
393
394 return (object) [];
395 }
396
397 /**
398 * If the plugin is lacking an active subscription, throw a warning.
399 *
400 * @param array $plugin_data The data for the plugin in this row.
401 *
402 * @return void
403 */
404 public function expired_subscription_warning( $plugin_data ) {
405 $subscription = $this->get_subscription( $plugin_data['slug'] );
406 if ( $subscription && $this->has_subscription_expired( $subscription ) ) {
407 $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'];
408
409 $sale_copy = '';
410 if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-2023-promotion' ) ) {
411 $sale_copy = sprintf(
412 /* translators: %1$s is a <br> tag. */
413 esc_html__( '%1$s Now with 30%% Black Friday Discount!', 'wordpress-seo' ),
414 '<br>'
415 );
416 }
417 echo '<br><br>';
418 echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> '
419 . sprintf(
420 /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
421 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' ),
422 esc_html( $plugin_data['name'] ),
423 '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">',
424 '</a>'
425 )
426 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
427 . $sale_copy
428 . '</strong>';
429 }
430 }
431
432 /**
433 * Checks if there are any installed addons.
434 *
435 * @return bool True when there are installed Yoast addons.
436 */
437 public function has_installed_addons() {
438 $installed_addons = $this->get_installed_addons();
439
440 return ! empty( $installed_addons );
441 }
442
443 /**
444 * Checks if the plugin is installed and activated in WordPress.
445 *
446 * @param string $slug The class' slug.
447 *
448 * @return bool True when installed and activated.
449 */
450 public function is_installed( $slug ) {
451 $slug_to_class_map = [
452 static::PREMIUM_SLUG => 'WPSEO_Premium',
453 static::NEWS_SLUG => 'WPSEO_News',
454 static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
455 static::VIDEO_SLUG => 'WPSEO_Video_Sitemap',
456 static::LOCAL_SLUG => 'WPSEO_Local_Core',
457 ];
458
459 if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
460 return false;
461 }
462
463 return class_exists( $slug_to_class_map[ $slug ] );
464 }
465
466 /**
467 * Validates the addons and show a notice for the ones that are invalid.
468 *
469 * @return void
470 */
471 public function validate_addons() {
472 $notification_center = Yoast_Notification_Center::get();
473
474 if ( $notification_center === null ) {
475 return;
476 }
477
478 foreach ( $this->addon_details as $slug => $addon_info ) {
479 $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
480
481 // Add a notification when the installed plugin isn't activated in My Yoast.
482 if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
483 $notification_center->add_notification( $notification );
484
485 continue;
486 }
487
488 $notification_center->remove_notification( $notification );
489 }
490 }
491
492 /**
493 * Removes the site information transients.
494 *
495 * @codeCoverageIgnore
496 *
497 * @return void
498 */
499 public function remove_site_information_transients() {
500 delete_transient( self::SITE_INFORMATION_TRANSIENT );
501 delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
502 }
503
504 /**
505 * Creates an instance of Yoast_Notification.
506 *
507 * @param string $product_name The product to create the notification for.
508 * @param string $short_link The short link for the addon notification.
509 *
510 * @return Yoast_Notification The created notification.
511 */
512 protected function create_notification( $product_name, $short_link ) {
513 $notification_options = [
514 'type' => Yoast_Notification::ERROR,
515 'id' => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
516 'capabilities' => 'wpseo_manage_options',
517 ];
518
519 return new Yoast_Notification(
520 sprintf(
521 /* 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 */
522 __( '%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' ),
523 '<strong>',
524 $product_name,
525 '</strong>',
526 '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
527 'MyYoast',
528 '</a>',
529 $product_name
530 ),
531 $notification_options
532 );
533 }
534
535 /**
536 * Checks whether a plugin expiry date has been passed.
537 *
538 * @param stdClass $subscription Plugin subscription.
539 *
540 * @return bool Has the plugin expired.
541 */
542 protected function has_subscription_expired( $subscription ) {
543 return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
544 }
545
546 /**
547 * Converts a subscription to plugin based format.
548 *
549 * @param stdClass $subscription The subscription to convert.
550 * @param stdClass|null $yoast_free_data The Yoast Free's data.
551 * @param bool $plugin_info Whether we're in the plugin information modal.
552 * @param string $plugin_file The plugin filename.
553 *
554 * @return stdClass The converted subscription.
555 */
556 protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
557 $changelog = '';
558 if ( isset( $subscription->product->changelog ) ) {
559 // We need to replace h2's and h3's with h4's because the styling expects that.
560 $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
561 $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
562 }
563
564 // 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.
565 $defaults = [
566 // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
567 'requires' => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
568 ];
569
570 return (object) [
571 'new_version' => ( $subscription->product->version ?? '' ),
572 'name' => $subscription->product->name,
573 'slug' => $subscription->product->slug,
574 'plugin' => $plugin_file,
575 'url' => $subscription->product->store_url,
576 'last_update' => $subscription->product->last_updated,
577 'homepage' => $subscription->product->store_url,
578 'download_link' => $subscription->product->download,
579 'package' => $subscription->product->download,
580 'sections' => [
581 'changelog' => $changelog,
582 'support' => $this->get_support_section(),
583 ],
584 'icons' => [
585 '2x' => $this->get_icon( $subscription->product->slug ),
586 ],
587 'update_supported' => true,
588 'banners' => $this->get_banners( $subscription->product->slug ),
589 // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
590 'tested' => YOAST_SEO_WP_TESTED,
591 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ),
592 'requires_php' => YOAST_SEO_PHP_REQUIRED,
593 ];
594 }
595
596 /**
597 * Returns the plugin's icon URL.
598 *
599 * @param string $slug The plugin slug.
600 *
601 * @return string The icon URL for this plugin.
602 */
603 protected function get_icon( $slug ) {
604 switch ( $slug ) {
605 case self::LOCAL_SLUG:
606 return 'https://yoa.st/local-seo-icon';
607 case self::NEWS_SLUG:
608 return 'https://yoa.st/news-seo-icon';
609 case self::PREMIUM_SLUG:
610 return 'https://yoa.st/yoast-seo-icon';
611 case self::VIDEO_SLUG:
612 return 'https://yoa.st/video-seo-icon';
613 case self::WOOCOMMERCE_SLUG:
614 return 'https://yoa.st/woo-seo-icon';
615 }
616 }
617
618 /**
619 * Return an array of plugin banner URLs.
620 *
621 * @param string $slug The plugin slug.
622 *
623 * @return string[]
624 */
625 protected function get_banners( $slug ) {
626 switch ( $slug ) {
627 case self::LOCAL_SLUG:
628 return [
629 'high' => 'https://yoa.st/yoast-seo-banner-local',
630 'low' => 'https://yoa.st/yoast-seo-banner-low-local',
631 ];
632 case self::NEWS_SLUG:
633 return [
634 'high' => 'https://yoa.st/yoast-seo-banner-news',
635 'low' => 'https://yoa.st/yoast-seo-banner-low-news',
636 ];
637 case self::PREMIUM_SLUG:
638 return [
639 'high' => 'https://yoa.st/yoast-seo-banner-premium',
640 'low' => 'https://yoa.st/yoast-seo-banner-low-premium',
641 ];
642 case self::VIDEO_SLUG:
643 return [
644 'high' => 'https://yoa.st/yoast-seo-banner-video',
645 'low' => 'https://yoa.st/yoast-seo-banner-low-video',
646 ];
647 case self::WOOCOMMERCE_SLUG:
648 return [
649 'high' => 'https://yoa.st/yoast-seo-banner-woo',
650 'low' => 'https://yoa.st/yoast-seo-banner-low-woo',
651 ];
652 }
653 }
654
655 /**
656 * Checks if the given plugin_file belongs to a Yoast addon.
657 *
658 * @param string $plugin_file Path to the plugin.
659 *
660 * @return bool True when plugin file is for a Yoast addon.
661 */
662 protected function is_yoast_addon( $plugin_file ) {
663 return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
664 }
665
666 /**
667 * Retrieves the addon slug by given plugin file path.
668 *
669 * @param string $plugin_file The file path to the plugin.
670 *
671 * @return string The slug when found or empty string when not.
672 */
673 protected function get_slug_by_plugin_file( $plugin_file ) {
674 $addons = self::$addons;
675
676 // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
677 if ( YoastSEO()->helpers->product->is_premium() ) {
678 $addons['wp-seo.php'] = self::FREE_SLUG;
679 }
680
681 foreach ( $addons as $addon => $addon_slug ) {
682 if ( strpos( $plugin_file, $addon ) !== false ) {
683 return $addon_slug;
684 }
685 }
686
687 return '';
688 }
689
690 /**
691 * Retrieves the installed Yoast addons.
692 *
693 * @return array The installed plugins.
694 */
695 protected function get_installed_addons() {
696 return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY );
697 }
698
699 /**
700 * Retrieves a list of active addons.
701 *
702 * @return array The active addons.
703 */
704 protected function get_active_addons() {
705 return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
706 }
707
708 /**
709 * Retrieves the current sites from the API.
710 *
711 * @codeCoverageIgnore
712 *
713 * @return bool|stdClass Object when request is successful. False if not.
714 */
715 protected function request_current_sites() {
716 $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
717 if ( $api_request->fire() ) {
718 return $api_request->get_response();
719 }
720
721 return $this->get_site_information_default();
722 }
723
724 /**
725 * Retrieves the transient value with the site information.
726 *
727 * @codeCoverageIgnore
728 *
729 * @return stdClass|false The transient value.
730 */
731 protected function get_site_information_transient() {
732 global $pagenow;
733
734 // Force re-check on license & dashboard pages.
735 $current_page = null;
736 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
737 if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) {
738 // 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.
739 $current_page = wp_unslash( $_GET['page'] );
740 }
741
742 // Check whether the licenses are valid or whether we need to show notifications.
743 $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE );
744
745 // Also do a fresh request on Plugins & Core Update pages.
746 $quick = $quick || $pagenow === 'plugins.php';
747 $quick = $quick || $pagenow === 'update-core.php';
748
749 if ( $quick ) {
750 return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
751 }
752
753 return get_transient( self::SITE_INFORMATION_TRANSIENT );
754 }
755
756 /**
757 * Sets the site information transient.
758 *
759 * @codeCoverageIgnore
760 *
761 * @param stdClass $site_information The site information to save.
762 *
763 * @return void
764 */
765 protected function set_site_information_transient( $site_information ) {
766 set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
767 set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 );
768 }
769
770 /**
771 * Retrieves all installed WordPress plugins.
772 *
773 * @codeCoverageIgnore
774 *
775 * @return array The plugins.
776 */
777 protected function get_plugins() {
778 if ( ! function_exists( 'get_plugins' ) ) {
779 require_once ABSPATH . 'wp-admin/includes/plugin.php';
780 }
781
782 return get_plugins();
783 }
784
785 /**
786 * Checks if the given plugin file belongs to an active plugin.
787 *
788 * @codeCoverageIgnore
789 *
790 * @param string $plugin_file The file path to the plugin.
791 *
792 * @return bool True when plugin is active.
793 */
794 protected function is_plugin_active( $plugin_file ) {
795 return is_plugin_active( $plugin_file );
796 }
797
798 /**
799 * Returns an object with no subscriptions.
800 *
801 * @codeCoverageIgnore
802 *
803 * @return stdClass Site information.
804 */
805 protected function get_site_information_default() {
806 return (object) [
807 'url' => WPSEO_Utils::get_home_url(),
808 'subscriptions' => [],
809 ];
810 }
811
812 /**
813 * Maps the plugin API response.
814 *
815 * @param object $site_information Site information as received from the API.
816 *
817 * @return stdClass Mapped site information.
818 */
819 protected function map_site_information( $site_information ) {
820 return (object) [
821 'url' => $site_information->url,
822 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
823 ];
824 }
825
826 /**
827 * Maps a plugin subscription.
828 *
829 * @param object $subscription Subscription information as received from the API.
830 *
831 * @return stdClass Mapped subscription.
832 */
833 protected function map_subscription( $subscription ) {
834 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties.
835 return (object) [
836 'renewal_url' => $subscription->renewalUrl,
837 'expiry_date' => $subscription->expiryDate,
838 'product' => (object) [
839 'version' => $subscription->product->version,
840 'name' => $subscription->product->name,
841 'slug' => $subscription->product->slug,
842 'last_updated' => $subscription->product->lastUpdated,
843 'store_url' => $subscription->product->storeUrl,
844 // Ternary operator is necessary because download can be undefined.
845 'download' => ( $subscription->product->download ?? null ),
846 'changelog' => $subscription->product->changelog,
847 ],
848 ];
849 // phpcs:enable
850 }
851
852 /**
853 * Retrieves the site information.
854 *
855 * @return stdClass The site information.
856 */
857 private function get_site_information() {
858 if ( ! $this->has_installed_addons() ) {
859 return $this->get_site_information_default();
860 }
861
862 return $this->get_myyoast_site_information();
863 }
864
865 /**
866 * Retrieves the contents for the support section.
867 *
868 * @return string The support section content.
869 */
870 protected function get_support_section() {
871 return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>'
872 . '<p>'
873 /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */
874 . 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>' )
875 . ' '
876 /* translators: %s expands to a mailto support link. */
877 . 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>' )
878 . '</p>';
879 }
880 }
881