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

Yoast / wordpress-seo / 56db0408fe2a0dbffe1c2c6cfad9b5c2f6941e34

14 Apr 2025 12:24PM UTC coverage: 52.454% (-2.1%) from 54.594%
56db0408fe2a0dbffe1c2c6cfad9b5c2f6941e34

Pull #22077

github

enricobattocchi
Adjust carryforward in Coveralls action
Pull Request #22077: Drop compatibility with PHP 7.2 and 7.3

7827 of 13877 branches covered (56.4%)

Branch coverage included in aggregate %.

29025 of 56379 relevant lines covered (51.48%)

42277.18 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

281
                if ( $this->site_information ) {
10✔
282
                        return $this->site_information;
10✔
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 ) {
6✔
305
                $subscription = $this->get_subscription( $slug );
6✔
306

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

312
                return ! $this->has_subscription_expired( $subscription );
4✔
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 ) {
14✔
323
                global $wp_version;
14✔
324

325
                if ( empty( $data ) ) {
14✔
326
                        return $data;
6✔
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 );
8✔
331

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

336
                        if ( ! $subscription ) {
6✔
337
                                continue;
4✔
338
                        }
339

340
                        $plugin_data = $this->convert_subscription_to_plugin( $subscription, $yoast_free_data, false, $plugin_file );
2✔
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;
2✔
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, '<' ) ) {
2✔
347
                                // If we haven't retrieved the Yoast Free requirements for the WP version yet, do nothing. The next run will probably get us that information.
348
                                if ( $plugin_data->requires === null ) {
2✔
349
                                        continue;
×
350
                                }
351

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

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

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

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

373
                return $data;
8✔
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() {
2✔
436
                $installed_addons = $this->get_installed_addons();
2✔
437

438
                return ! empty( $installed_addons );
2✔
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 ) {
8✔
511
                $notification_options = [
8✔
512
                        'type'         => Yoast_Notification::ERROR,
8✔
513
                        'id'           => 'wpseo-dismiss-' . sanitize_title_with_dashes( $product_name, null, 'save' ),
8✔
514
                        'capabilities' => 'wpseo_manage_options',
8✔
515
                ];
8✔
516

517
                return new Yoast_Notification(
8✔
518
                        sprintf(
8✔
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' ),
8✔
521
                                '<strong>',
8✔
522
                                $product_name,
8✔
523
                                '</strong>',
8✔
524
                                '<a href="' . WPSEO_Shortlinker::get( $short_link ) . '" target="_blank">',
8✔
525
                                'MyYoast',
8✔
526
                                '</a>',
8✔
527
                                $product_name
8✔
528
                        ),
8✔
529
                        $notification_options
8✔
530
                );
8✔
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 = '' ) {
4✔
555
                $changelog = '';
4✔
556
                if ( isset( $subscription->product->changelog ) ) {
4✔
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 ) );
2✔
559
                        $changelog = str_replace( '</h3', '</h4', str_replace( '<h3', '<h4', $changelog ) );
2✔
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 = [
4✔
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,
4✔
566
                ];
4✔
567

568
                return (object) [
4✔
569
                        'new_version'      => ( $subscription->product->version ?? '' ),
4✔
570
                        'name'             => $subscription->product->name,
4✔
571
                        'slug'             => $subscription->product->slug,
4✔
572
                        'plugin'           => $plugin_file,
4✔
573
                        'url'              => $subscription->product->store_url,
4✔
574
                        'last_update'      => $subscription->product->last_updated,
4✔
575
                        'homepage'         => $subscription->product->store_url,
4✔
576
                        'download_link'    => $subscription->product->download,
4✔
577
                        'package'          => $subscription->product->download,
4✔
578
                        'sections'         => [
4✔
579
                                'changelog' => $changelog,
4✔
580
                                'support'   => $this->get_support_section(),
4✔
581
                        ],
4✔
582
                        'icons'            => [
4✔
583
                                '2x' => $this->get_icon( $subscription->product->slug ),
4✔
584
                        ],
4✔
585
                        'update_supported' => true,
4✔
586
                        'banners'          => $this->get_banners( $subscription->product->slug ),
4✔
587
                        // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults.
588
                        'tested'           => YOAST_SEO_WP_TESTED,
4✔
589
                        'requires'         => ( $yoast_free_data->requires ?? $defaults['requires'] ),
4✔
590
                        'requires_php'     => YOAST_SEO_PHP_REQUIRED,
4✔
591
                ];
4✔
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 ) {
2✔
661
                return $this->get_slug_by_plugin_file( $plugin_file ) !== '';
2✔
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 ) {
2✔
672
                $addons = self::$addons;
2✔
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() ) {
2✔
676
                        $addons['wp-seo.php'] = self::FREE_SLUG;
×
677
                }
678

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

685
                return '';
2✔
686
        }
687

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

697
        /**
698
         * Retrieves a list of active addons.
699
         *
700
         * @return array The active addons.
701
         */
702
        protected function get_active_addons() {
2✔
703
                return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY );
2✔
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() {
8✔
856
                if ( ! $this->has_installed_addons() ) {
8✔
857
                        return $this->get_site_information_default();
2✔
858
                }
859

860
                return $this->get_myyoast_site_information();
6✔
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
}
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