• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / facc8f018e2384ec4943ce98eecb050104439f58

27 Aug 2025 02:15PM UTC coverage: 53.206% (-0.07%) from 53.276%
facc8f018e2384ec4943ce98eecb050104439f58

push

github

web-flow
Merge pull request #22535 from Yoast/feature/bf-2025-c

Feature/bf 2025 c

8344 of 15215 branches covered (54.84%)

Branch coverage included in aggregate %.

34 of 172 new or added lines in 36 files covered. (19.77%)

9 existing lines in 6 files now uncovered.

31641 of 59937 relevant lines covered (52.79%)

39836.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.17
/inc/class-addon-manager.php
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() {
8✔
157
                return $this->get_site_information()->subscriptions;
8✔
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() {
2✔
166
                return self::$addons;
2✔
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 ) {
4✔
177
                $plugins            = $this->get_plugins();
4✔
178
                $plugin_files       = array_keys( $plugins );
4✔
179
                $target_plugin_file = array_search( $plugin_slug, $this->get_addon_filenames(), true );
4✔
180

181
                if ( ! $target_plugin_file ) {
4✔
182
                        return false;
2✔
183
                }
184

185
                foreach ( $plugin_files as $plugin_file ) {
2✔
186
                        if ( strpos( $plugin_file, $target_plugin_file ) !== false ) {
2✔
187
                                return $plugin_file;
2✔
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 ) {
4✔
202
                foreach ( $this->get_subscriptions() as $subscription ) {
4✔
203
                        if ( $subscription->product->slug === $slug ) {
4✔
204
                                return $subscription;
2✔
205
                        }
206
                }
207

208
                return false;
2✔
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() {
2✔
217
                $active_addons      = array_keys( $this->get_active_addons() );
2✔
218
                $subscription_slugs = array_map( [ $this, 'get_slug_by_plugin_file' ], $active_addons );
2✔
219
                $subscriptions      = [];
2✔
220
                foreach ( $subscription_slugs as $subscription_slug ) {
2✔
221
                        $subscriptions[ $subscription_slug ] = $this->get_subscription( $subscription_slug );
2✔
222
                }
223

224
                return $subscriptions;
2✔
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() {
2✔
233
                $addon_versions = [];
2✔
234
                foreach ( $this->get_installed_addons() as $plugin_file => $installed_addon ) {
2✔
235
                        $addon_versions[ $this->get_slug_by_plugin_file( $plugin_file ) ] = $installed_addon['Version'];
2✔
236
                }
237

238
                return $addon_versions;
2✔
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 ) {
8✔
251
                if ( $action !== 'plugin_information' ) {
8✔
252
                        return $data;
2✔
253
                }
254

255
                if ( ! isset( $args->slug ) ) {
6✔
256
                        return $data;
2✔
257
                }
258

259
                $subscription = $this->get_subscription( $args->slug );
4✔
260
                if ( ! $subscription ) {
4✔
261
                        return $data;
2✔
262
                }
263

264
                $data = $this->convert_subscription_to_plugin( $subscription, null, true );
2✔
265

266
                if ( $this->has_subscription_expired( $subscription ) ) {
2✔
267
                        unset( $data->package, $data->download_link );
×
268
                }
269

270
                return $data;
2✔
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() {
10✔
279
                if ( $this->site_information === null ) {
10✔
280
                        $this->site_information = $this->get_site_information_transient();
10✔
281
                }
282

283
                if ( $this->site_information ) {
10✔
284
                        return $this->site_information;
10✔
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 ) {
6✔
307
                $subscription = $this->get_subscription( $slug );
6✔
308

309
                // An non-existing subscription is never valid.
310
                if ( ! $subscription ) {
6✔
311
                        return false;
2✔
312
                }
313

314
                return ! $this->has_subscription_expired( $subscription );
4✔
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 ) {
14✔
325
                global $wp_version;
14✔
326

327
                if ( empty( $data ) ) {
14✔
328
                        return $data;
6✔
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 );
8✔
333

334
                foreach ( $this->get_installed_addons() as $plugin_file => $installed_plugin ) {
8✔
335
                        $subscription_slug = $this->get_slug_by_plugin_file( $plugin_file );
6✔
336
                        $subscription      = $this->get_subscription( $subscription_slug );
6✔
337

338
                        if ( ! $subscription ) {
6✔
339
                                continue;
4✔
340
                        }
341

342
                        $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file );
2✔
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;
2✔
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, '<' ) ) {
2✔
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 ) {
2✔
351
                                        continue;
×
352
                                }
353

354
                                if ( version_compare( $plugin_data->requires, $wp_version, '<=' ) ) {
2✔
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;
2✔
357
                                        $data->response[ $plugin_file ] = $plugin_data;
2✔
358

359
                                        if ( $this->has_subscription_expired( $subscription ) ) {
2✔
360
                                                unset( $data->response[ $plugin_file ]->package, $data->response[ $plugin_file ]->download_link );
×
361
                                        }
362
                                }
363
                        }
364

365
                        if ( $is_no_update ) {
2✔
366
                                // Still convert subscription when no updates is available.
367
                                $data->no_update[ $plugin_file ] = $plugin_data;
2✔
368

369
                                if ( $this->has_subscription_expired( $subscription ) ) {
2✔
370
                                        unset( $data->no_update[ $plugin_file ]->package, $data->no_update[ $plugin_file ]->download_link );
×
371
                                }
372
                        }
373
                }
374

375
                return $data;
8✔
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 = '';
×
NEW
410
                        if ( YoastSEO()->classes->get( Promotion_Manager::class )->is( 'black-friday-promotion' ) ) {
×
411
                                $sale_copy = sprintf(
×
412
                                /* translators: %1$s and %2$s are a <span> opening and closing tag. */
NEW
413
                                        esc_html__( '%1$s30%% OFF - Black Friday %2$s', 'wordpress-seo' ),
×
NEW
414
                                        '<span class="yoast-update-plugin-bf-sale-badge">',
×
NEW
415
                                        '</span>'
×
UNCOV
416
                                );
×
417
                        }
418
                        echo '<br><br>';
×
419
                        echo '<strong><span class="yoast-dashicons-notice warning dashicons dashicons-warning"></span> '
×
420
                                . sprintf(
×
421
                                        /* translators: %1$s is the plugin name, %2$s and %3$s are a link. */
NEW
422
                                        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' ),
×
423
                                        esc_html( $plugin_data['name'] ),
×
424
                                        '<a href="' . esc_url( WPSEO_Shortlinker::get( $addon_link ) ) . '">',
×
425
                                        '</a>'
×
426
                                )
×
427
                                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is escaped above.
×
428
                                . $sale_copy
×
429
                                . '</strong>';
×
430
                }
431
        }
432

433
        /**
434
         * Checks if there are any installed addons.
435
         *
436
         * @return bool True when there are installed Yoast addons.
437
         */
438
        public function has_installed_addons() {
2✔
439
                $installed_addons = $this->get_installed_addons();
2✔
440

441
                return ! empty( $installed_addons );
2✔
442
        }
443

444
        /**
445
         * Checks if the plugin is installed and activated in WordPress.
446
         *
447
         * @param string $slug The class' slug.
448
         *
449
         * @return bool True when installed and activated.
450
         */
451
        public function is_installed( $slug ) {
×
452
                $slug_to_class_map = [
×
453
                        static::PREMIUM_SLUG     => 'WPSEO_Premium',
×
454
                        static::NEWS_SLUG        => 'WPSEO_News',
×
455
                        static::WOOCOMMERCE_SLUG => 'Yoast_WooCommerce_SEO',
×
456
                        static::VIDEO_SLUG       => 'WPSEO_Video_Sitemap',
×
457
                        static::LOCAL_SLUG       => 'WPSEO_Local_Core',
×
458
                ];
×
459

460
                if ( ! isset( $slug_to_class_map[ $slug ] ) ) {
×
461
                        return false;
×
462
                }
463

464
                return class_exists( $slug_to_class_map[ $slug ] );
×
465
        }
466

467
        /**
468
         * Validates the addons and show a notice for the ones that are invalid.
469
         *
470
         * @return void
471
         */
472
        public function validate_addons() {
×
473
                $notification_center = Yoast_Notification_Center::get();
×
474

475
                if ( $notification_center === null ) {
×
476
                        return;
×
477
                }
478

479
                foreach ( $this->addon_details as $slug => $addon_info ) {
×
480
                        $notification = $this->create_notification( $addon_info['name'], $addon_info['short_link_activation'] );
×
481

482
                        // Add a notification when the installed plugin isn't activated in My Yoast.
483
                        if ( $this->is_installed( $slug ) && ! $this->has_valid_subscription( $slug ) ) {
×
484
                                $notification_center->add_notification( $notification );
×
485

486
                                continue;
×
487
                        }
488

489
                        $notification_center->remove_notification( $notification );
×
490
                }
491
        }
492

493
        /**
494
         * Removes the site information transients.
495
         *
496
         * @codeCoverageIgnore
497
         *
498
         * @return void
499
         */
500
        public function remove_site_information_transients() {
501
                delete_transient( self::SITE_INFORMATION_TRANSIENT );
502
                delete_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
503
        }
504

505
        /**
506
         * Creates an instance of Yoast_Notification.
507
         *
508
         * @param string $product_name The product to create the notification for.
509
         * @param string $short_link   The short link for the addon notification.
510
         *
511
         * @return Yoast_Notification The created notification.
512
         */
513
        protected function create_notification( $product_name, $short_link ) {
8✔
514
                $notification_options = [
8✔
515
                        'type'         => Yoast_Notification::ERROR,
8✔
516
                        'id'           => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
8✔
517
                        'capabilities' => 'wpseo_manage_options',
8✔
518
                ];
8✔
519

520
                return new Yoast_Notification(
8✔
521
                        sprintf(
8✔
522
                        /* 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  */
523
                                __( '%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' ),
8✔
524
                                '<strong>',
8✔
525
                                $product_name,
8✔
526
                                '</strong>',
8✔
527
                                '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
8✔
528
                                'MyYoast',
8✔
529
                                '</a>',
8✔
530
                                $product_name
8✔
531
                        ),
8✔
532
                        $notification_options
8✔
533
                );
8✔
534
        }
535

536
        /**
537
         * Checks whether a plugin expiry date has been passed.
538
         *
539
         * @param stdClass $subscription Plugin subscription.
540
         *
541
         * @return bool Has the plugin expired.
542
         */
543
        protected function has_subscription_expired( $subscription ) {
×
544
                return ( strtotime( $subscription->expiry_date ) - time() ) < 0;
×
545
        }
546

547
        /**
548
         * Converts a subscription to plugin based format.
549
         *
550
         * @param stdClass      $subscription    The subscription to convert.
551
         * @param stdClass|null $yoast_free_data The Yoast Free's data.
552
         * @param bool          $plugin_info     Whether we're in the plugin information modal.
553
         * @param string        $plugin_file     The plugin filename.
554
         *
555
         * @return stdClass The converted subscription.
556
         */
557
        protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) {
4✔
558
                $changelog = '';
4✔
559
                if ( isset( $subscription->product->changelog ) ) {
4✔
560
                        // We need to replace h2's and h3's with h4's because the styling expects that.
561
                        $changelog = str_replace( '</h2', '</h4', str_replace( '<h2', '<h4', $subscription->product->changelog ) );
2✔
562
                        $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
2✔
563
                }
564

565
                // 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.
566
                $defaults = [
4✔
567
                        // It can be expanded if we have the 'tested' and 'requires_php' data be returned from wp.org in the future.
568
                        'requires'     => ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null,
4✔
569
                ];
4✔
570

571
                return (object) [
4✔
572
                        'new_version'      => ( $subscription->product->version ?? '' ),
4✔
573
                        'name'             => $subscription->product->name,
4✔
574
                        'slug'             => $subscription->product->slug,
4✔
575
                        'plugin'           => $plugin_file,
4✔
576
                        'url'              => $subscription->product->store_url,
4✔
577
                        'last_update'      => $subscription->product->last_updated,
4✔
578
                        'homepage'         => $subscription->product->store_url,
4✔
579
                        'download_link'    => $subscription->product->download,
4✔
580
                        'package'          => $subscription->product->download,
4✔
581
                        'sections'         => [
4✔
582
                                'changelog' => $changelog,
4✔
583
                                'support'   => $this->get_support_section(),
4✔
584
                        ],
4✔
585
                        'icons'            => [
4✔
586
                                '2x' => $this->get_icon( $subscription->product->slug ),
4✔
587
                        ],
4✔
588
                        'update_supported' => true,
4✔
589
                        'banners'          => $this->get_banners( $subscription->product->slug ),
4✔
590
                        // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
591
                        'tested'           => YOAST_SEO_WP_TESTED,
4✔
592
                        'requires'         => ( $yoast_free_data->requires ?? $defaults['requires'] ),
4✔
593
                        'requires_php'     => YOAST_SEO_PHP_REQUIRED,
4✔
594
                ];
4✔
595
        }
596

597
        /**
598
         * Returns the plugin's icon URL.
599
         *
600
         * @param string $slug The plugin slug.
601
         *
602
         * @return string The icon URL for this plugin.
603
         */
604
        protected function get_icon( $slug ) {
×
605
                switch ( $slug ) {
606
                        case self::LOCAL_SLUG:
607
                                return 'https://yoa.st/local-seo-icon';
×
608
                        case self::NEWS_SLUG:
609
                                return 'https://yoa.st/news-seo-icon';
×
610
                        case self::PREMIUM_SLUG:
611
                                return 'https://yoa.st/yoast-seo-icon';
×
612
                        case self::VIDEO_SLUG:
613
                                return 'https://yoa.st/video-seo-icon';
×
614
                        case self::WOOCOMMERCE_SLUG:
615
                                return 'https://yoa.st/woo-seo-icon';
×
616
                }
617
        }
618

619
        /**
620
         * Return an array of plugin banner URLs.
621
         *
622
         * @param string $slug The plugin slug.
623
         *
624
         * @return string[]
625
         */
626
        protected function get_banners( $slug ) {
×
627
                switch ( $slug ) {
628
                        case self::LOCAL_SLUG:
629
                                return [
×
630
                                        'high' => 'https://yoa.st/yoast-seo-banner-local',
×
631
                                        'low'  => 'https://yoa.st/yoast-seo-banner-low-local',
×
632
                                ];
×
633
                        case self::NEWS_SLUG:
634
                                return [
×
635
                                        'high' => 'https://yoa.st/yoast-seo-banner-news',
×
636
                                        'low'  => 'https://yoa.st/yoast-seo-banner-low-news',
×
637
                                ];
×
638
                        case self::PREMIUM_SLUG:
639
                                return [
×
640
                                        'high' => 'https://yoa.st/yoast-seo-banner-premium',
×
641
                                        'low'  => 'https://yoa.st/yoast-seo-banner-low-premium',
×
642
                                ];
×
643
                        case self::VIDEO_SLUG:
644
                                return [
×
645
                                        'high' => 'https://yoa.st/yoast-seo-banner-video',
×
646
                                        'low'  => 'https://yoa.st/yoast-seo-banner-low-video',
×
647
                                ];
×
648
                        case self::WOOCOMMERCE_SLUG:
649
                                return [
×
650
                                        'high' => 'https://yoa.st/yoast-seo-banner-woo',
×
651
                                        'low'  => 'https://yoa.st/yoast-seo-banner-low-woo',
×
652
                                ];
×
653
                }
654
        }
655

656
        /**
657
         * Checks if the given plugin_file belongs to a Yoast addon.
658
         *
659
         * @param string $plugin_file Path to the plugin.
660
         *
661
         * @return bool True when plugin file is for a Yoast addon.
662
         */
663
        protected function is_yoast_addon( $plugin_file ) {
2✔
664
                return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
2✔
665
        }
666

667
        /**
668
         * Retrieves the addon slug by given plugin file path.
669
         *
670
         * @param string $plugin_file The file path to the plugin.
671
         *
672
         * @return string The slug when found or empty string when not.
673
         */
674
        protected function get_slug_by_plugin_file( $plugin_file ) {
2✔
675
                $addons = self::$addons;
2✔
676

677
                // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations.
678
                if ( YoastSEO()->helpers->product->is_premium() ) {
2✔
679
                        $addons['wp-seo.php'] = self::FREE_SLUG;
×
680
                }
681

682
                foreach ( $addons as $addon => $addon_slug ) {
2✔
683
                        if ( strpos( $plugin_file, $addon ) !== false ) {
2✔
684
                                return $addon_slug;
2✔
685
                        }
686
                }
687

688
                return '';
2✔
689
        }
690

691
        /**
692
         * Retrieves the installed Yoast addons.
693
         *
694
         * @return array The installed plugins.
695
         */
696
        protected function get_installed_addons() {
4✔
697
                return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY );
4✔
698
        }
699

700
        /**
701
         * Retrieves a list of active addons.
702
         *
703
         * @return array The active addons.
704
         */
705
        protected function get_active_addons() {
2✔
706
                return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
2✔
707
        }
708

709
        /**
710
         * Retrieves the current sites from the API.
711
         *
712
         * @codeCoverageIgnore
713
         *
714
         * @return bool|stdClass Object when request is successful. False if not.
715
         */
716
        protected function request_current_sites() {
717
                $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' );
718
                if ( $api_request->fire() ) {
719
                        return $api_request->get_response();
720
                }
721

722
                return $this->get_site_information_default();
723
        }
724

725
        /**
726
         * Retrieves the transient value with the site information.
727
         *
728
         * @codeCoverageIgnore
729
         *
730
         * @return stdClass|false The transient value.
731
         */
732
        protected function get_site_information_transient() {
733
                global $pagenow;
734

735
                // Force re-check on license & dashboard pages.
736
                $current_page = null;
737
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
738
                if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) {
739
                        // 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.
740
                        $current_page = wp_unslash( $_GET['page'] );
741
                }
742

743
                // Check whether the licenses are valid or whether we need to show notifications.
744
                $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE );
745

746
                // Also do a fresh request on Plugins & Core Update pages.
747
                $quick = $quick || $pagenow === 'plugins.php';
748
                $quick = $quick || $pagenow === 'update-core.php';
749

750
                if ( $quick ) {
751
                        return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK );
752
                }
753

754
                return get_transient( self::SITE_INFORMATION_TRANSIENT );
755
        }
756

757
        /**
758
         * Sets the site information transient.
759
         *
760
         * @codeCoverageIgnore
761
         *
762
         * @param stdClass $site_information The site information to save.
763
         *
764
         * @return void
765
         */
766
        protected function set_site_information_transient( $site_information ) {
767
                set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS );
768
                set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 );
769
        }
770

771
        /**
772
         * Retrieves all installed WordPress plugins.
773
         *
774
         * @codeCoverageIgnore
775
         *
776
         * @return array The plugins.
777
         */
778
        protected function get_plugins() {
779
                if ( ! function_exists( 'get_plugins' ) ) {
780
                        require_once ABSPATH . 'wp-admin/includes/plugin.php';
781
                }
782

783
                return get_plugins();
784
        }
785

786
        /**
787
         * Checks if the given plugin file belongs to an active plugin.
788
         *
789
         * @codeCoverageIgnore
790
         *
791
         * @param string $plugin_file The file path to the plugin.
792
         *
793
         * @return bool True when plugin is active.
794
         */
795
        protected function is_plugin_active( $plugin_file ) {
796
                return is_plugin_active( $plugin_file );
797
        }
798

799
        /**
800
         * Returns an object with no subscriptions.
801
         *
802
         * @codeCoverageIgnore
803
         *
804
         * @return stdClass Site information.
805
         */
806
        protected function get_site_information_default() {
807
                return (object) [
808
                        'url'           => WPSEO_Utils::get_home_url(),
809
                        'subscriptions' => [],
810
                ];
811
        }
812

813
        /**
814
         * Maps the plugin API response.
815
         *
816
         * @param object $site_information Site information as received from the API.
817
         *
818
         * @return stdClass Mapped site information.
819
         */
820
        protected function map_site_information( $site_information ) {
×
821
                return (object) [
×
822
                        'url'           => $site_information->url,
×
823
                        'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ),
×
824
                ];
×
825
        }
826

827
        /**
828
         * Maps a plugin subscription.
829
         *
830
         * @param object $subscription Subscription information as received from the API.
831
         *
832
         * @return stdClass Mapped subscription.
833
         */
834
        protected function map_subscription( $subscription ) {
×
835
                // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties.
836
                return (object) [
×
837
                        'renewal_url' => $subscription->renewalUrl,
×
838
                        'expiry_date' => $subscription->expiryDate,
×
839
                        'product'     => (object) [
×
840
                                'version'      => $subscription->product->version,
×
841
                                'name'         => $subscription->product->name,
×
842
                                'slug'         => $subscription->product->slug,
×
843
                                'last_updated' => $subscription->product->lastUpdated,
×
844
                                'store_url'    => $subscription->product->storeUrl,
×
845
                                // Ternary operator is necessary because download can be undefined.
846
                                'download'     => ( $subscription->product->download ?? null ),
×
847
                                'changelog'    => $subscription->product->changelog,
×
848
                        ],
×
849
                ];
×
850
                // phpcs:enable
851
        }
852

853
        /**
854
         * Retrieves the site information.
855
         *
856
         * @return stdClass The site information.
857
         */
858
        private function get_site_information() {
8✔
859
                if ( ! $this->has_installed_addons() ) {
8✔
860
                        return $this->get_site_information_default();
2✔
861
                }
862

863
                return $this->get_myyoast_site_information();
6✔
864
        }
865

866
        /**
867
         * Retrieves the contents for the support section.
868
         *
869
         * @return string The support section content.
870
         */
871
        protected function get_support_section() {
×
872
                return '<h4>' . __( 'Need support?', 'wordpress-seo' ) . '</h4>'
×
873
                        . '<p>'
×
874
                        /* translators: 1: expands to <a> that refers to the help page, 2: </a> closing tag. */
×
875
                        . 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>' )
×
876
                        . ' '
×
877
                        /* translators: %s expands to a mailto support link. */
×
878
                        . 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>' )
×
879
                        . '</p>';
×
880
        }
881
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc