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

Yoast / wordpress-seo / 4ca456da6dba39a5252c0354fcc3fd5f002869ba

04 May 2026 01:33PM UTC coverage: 52.936%. Remained the same
4ca456da6dba39a5252c0354fcc3fd5f002869ba

push

github

web-flow
Merge pull request #23103 from Yoast/1123-make-limited-yoast-sidebar-elements-available-to-more-user-roles

1123 make limited yoast sidebar elements available to more user roles

8730 of 16414 branches covered (53.19%)

Branch coverage included in aggregate %.

5 of 22 new or added lines in 6 files covered. (22.73%)

8 existing lines in 2 files now uncovered.

34548 of 65342 relevant lines covered (52.87%)

46036.05 hits per line

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

0.0
/src/integrations/admin/helpscout-beacon.php
1
<?php
2

3
namespace Yoast\WP\SEO\Integrations\Admin;
4

5
use WPSEO_Addon_Manager;
6
use WPSEO_Admin_Asset_Manager;
7
use WPSEO_Tracking_Server_Data;
8
use WPSEO_Utils;
9
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
10
use Yoast\WP\SEO\Conditionals\User_Can_Manage_Wpseo_Options_Conditional;
11
use Yoast\WP\SEO\Config\Migration_Status;
12
use Yoast\WP\SEO\Helpers\Options_Helper;
13
use Yoast\WP\SEO\Integrations\Academy_Integration;
14
use Yoast\WP\SEO\Integrations\Integration_Interface;
15
use Yoast\WP\SEO\Integrations\Settings_Integration;
16
use Yoast\WP\SEO\Integrations\Support_Integration;
17
use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration;
18

19
/**
20
 * Class WPSEO_HelpScout
21
 */
22
class HelpScout_Beacon implements Integration_Interface {
23

24
        /**
25
         * The id for the beacon.
26
         *
27
         * @var string
28
         */
29
        protected $beacon_id = '2496aba6-0292-489c-8f5d-1c0fba417c2f';
30

31
        /**
32
         * The id for the beacon for users that have tracking on.
33
         *
34
         * @var string
35
         */
36
        protected $beacon_id_tracking_users = '6b8e74c5-aa81-4295-b97b-c2a62a13ea7f';
37

38
        /**
39
         * The id for the beacon for Premium users.
40
         *
41
         * @var string
42
         */
43
        protected $beacon_id_premium = '1ae02e91-5865-4f13-b220-7daed946ba25';
44

45
        /**
46
         * The id for the beacon for WooCommerce SEO users.
47
         *
48
         * @var string
49
         */
50
        protected $beacon_id_woocommerce = '8535d745-4e80-48b9-b211-087880aa857d';
51

52
        /**
53
         * The products the beacon is loaded for.
54
         *
55
         * @var array<string>
56
         */
57
        protected $products = [];
58

59
        /**
60
         * Whether to ask the user's consent before loading in HelpScout.
61
         *
62
         * @var bool
63
         */
64
        protected $ask_consent = true;
65

66
        /**
67
         * The options helper.
68
         *
69
         * @var Options_Helper
70
         */
71
        protected $options;
72

73
        /**
74
         * The addon manager.
75
         *
76
         * @var WPSEO_Addon_Manager
77
         */
78
        protected $addon_manager;
79

80
        /**
81
         * The array of pages we need to show the beacon on with their respective beacon IDs.
82
         *
83
         * @var array<string, string>
84
         */
85
        protected $pages_ids;
86

87
        /**
88
         * The array of pages we need to show the beacon on.
89
         *
90
         * @var array<string>
91
         */
92
        protected $base_pages = [
93
                'wpseo_dashboard',
94
                Settings_Integration::PAGE,
95
                Academy_Integration::PAGE,
96
                Support_Integration::PAGE,
97
                'wpseo_search_console',
98
                'wpseo_tools',
99
                Plans_Page_Integration::PAGE,
100
                'wpseo_workouts',
101
                'wpseo_integrations',
102
        ];
103

104
        /**
105
         * The current admin page
106
         *
107
         * @var string|null
108
         */
109
        protected $page;
110

111
        /**
112
         * The asset manager.
113
         *
114
         * @var WPSEO_Admin_Asset_Manager
115
         */
116
        protected $asset_manager;
117

118
        /**
119
         * The migration status object.
120
         *
121
         * @var Migration_Status
122
         */
123
        protected $migration_status;
124

125
        /**
126
         * Headless_Rest_Endpoints_Enabled_Conditional constructor.
127
         *
128
         * @param Options_Helper            $options          The options helper.
129
         * @param WPSEO_Admin_Asset_Manager $asset_manager    The asset manager.
130
         * @param Migration_Status          $migration_status The migrations status.
131
         * @param WPSEO_Addon_Manager       $addon_manager    The addon manager.
132
         */
133
        public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status, WPSEO_Addon_Manager $addon_manager ) {
×
134
                $this->options       = $options;
×
135
                $this->asset_manager = $asset_manager;
×
136
                $this->addon_manager = $addon_manager;
×
137
                $this->ask_consent   = ! $this->options->get( 'tracking' );
×
138
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
139
                if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) {
×
140
                        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
141
                        $this->page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) );
×
142
                }
143
                else {
144
                        $this->page = null;
×
145
                }
146
                $this->migration_status = $migration_status;
×
147

148
                $beacon_id = $this->get_beacon_id();
×
149
                foreach ( $this->base_pages as $page ) {
×
150
                        $this->pages_ids[ $page ] = $beacon_id;
×
151
                }
152
        }
153

154
        /**
155
         * {@inheritDoc}
156
         *
157
         * @return void
158
         */
159
        public function register_hooks() {
×
160
                \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] );
×
161
                \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] );
×
162
        }
163

164
        /**
165
         * Enqueues the HelpScout script.
166
         *
167
         * @return void
168
         */
169
        public function enqueue_help_scout_script() {
×
170
                // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon.
171
                $this->filter_settings();
×
172
                if ( ! $this->is_beacon_page() ) {
×
173
                        return;
×
174
                }
175

176
                $this->asset_manager->enqueue_script( 'help-scout-beacon' );
×
177
        }
178

179
        /**
180
         * Outputs a small piece of javascript for the beacon.
181
         *
182
         * @return void
183
         */
184
        public function output_beacon_js() {
×
185
                if ( ! $this->is_beacon_page() ) {
×
186
                        return;
×
187
                }
188

189
                \printf(
×
190
                        '<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>',
×
191
                        ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon',
×
192
                        \esc_html( $this->pages_ids[ $this->page ] ),
×
193
                        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode.
194
                        WPSEO_Utils::format_json_encode( (array) $this->get_session_data() ),
×
195
                );
×
196
        }
197

198
        /**
199
         * Checks if the current page is a page containing the beacon.
200
         *
201
         * @return bool
202
         */
203
        private function is_beacon_page() {
×
204
                $return = false;
×
205
                if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) {
×
206
                        $return = true;
×
207
                }
208

209
                /**
210
                 * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon.
211
                 *
212
                 * @param bool $show_beacon Whether we show the beacon or not.
213
                 */
214
                return \apply_filters( 'wpseo_helpscout_show_beacon', $return );
×
215
        }
216

217
        /**
218
         * Retrieves the identifying data.
219
         *
220
         * @return string The data to pass as identifying data.
221
         */
222
        protected function get_session_data() {
×
223
                // Short-circuit if we can get the needed data from a transient.
224
                $transient_data = \get_transient( 'yoast_beacon_session_data' );
×
225

226
                if ( \is_array( $transient_data ) ) {
×
227
                        return WPSEO_Utils::format_json_encode( $transient_data );
×
228
                }
229

230
                $current_user = \wp_get_current_user();
×
231

232
                // Do not make these strings translatable! They are for our support agents, the user won't see them!
233
                $data = \array_merge(
×
234
                        [
×
235
                                'name'               => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ),
×
236
                                'email'              => $current_user->user_email,
×
237
                                'Languages'          => $this->get_language_settings(),
×
238
                        ],
×
239
                        $this->get_server_info(),
×
240
                        [
×
241
                                'WordPress Version'    => $this->get_wordpress_version(),
×
242
                                'Active theme'         => $this->get_theme_info(),
×
243
                                'Active plugins'       => $this->get_active_plugins(),
×
244
                                'Must-use and dropins' => $this->get_mustuse_and_dropins(),
×
245
                                'Indexables status'    => $this->get_indexables_status(),
×
246
                        ],
×
247
                );
×
248

249
                if ( ! empty( $this->products ) ) {
×
250
                        $addon_manager = new WPSEO_Addon_Manager();
×
251
                        foreach ( $this->products as $product ) {
×
252
                                $subscription = $addon_manager->get_subscription( $product );
×
253

254
                                if ( ! $subscription ) {
×
255
                                        continue;
×
256
                                }
257

258
                                $data[ $subscription->product->name ] = $this->get_product_info( $subscription );
×
259
                        }
260
                }
261

262
                // Store the data in a transient for 5 minutes to prevent overhead on every backend pageload.
263
                \set_transient( 'yoast_beacon_session_data', $data, ( 5 * \MINUTE_IN_SECONDS ) );
×
264

265
                return WPSEO_Utils::format_json_encode( $data );
×
266
        }
267

268
        /**
269
         * Returns basic info about the server software.
270
         *
271
         * @return array<string, string>
272
         */
273
        private function get_server_info() {
×
274
                $server_tracking_data = new WPSEO_Tracking_Server_Data();
×
275
                $server_data          = $server_tracking_data->get();
×
276
                $server_data          = $server_data['server'];
×
277

278
                $fields_to_use = [
×
279
                        'Server IP'        => 'ip',
×
280
                        'PHP Version'      => 'PhpVersion',
×
281
                        'cURL Version'     => 'CurlVersion',
×
282
                ];
×
283

284
                $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . ' (SSL Support ' . $server_data['CurlVersion']['sslSupport'] . ')';
×
285

286
                $server_info = [];
×
287

288
                foreach ( $fields_to_use as $label => $field_to_use ) {
×
289
                        if ( isset( $server_data[ $field_to_use ] ) ) {
×
290
                                $server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] );
×
291
                        }
292
                }
293

294
                // Get the memory limits for the server and, if different, from WordPress as well.
295
                $memory_limit                 = \ini_get( 'memory_limit' );
×
296
                $server_info['Memory limits'] = 'Server memory limit: ' . $memory_limit;
×
297

298
                if ( $memory_limit !== \WP_MEMORY_LIMIT ) {
×
299
                        $server_info['Memory limits'] .= ', WP_MEMORY_LIMIT: ' . \WP_MEMORY_LIMIT;
×
300
                }
301

302
                if ( $memory_limit !== \WP_MAX_MEMORY_LIMIT ) {
×
303
                        $server_info['Memory limits'] .= ', WP_MAX_MEMORY_LIMIT: ' . \WP_MAX_MEMORY_LIMIT;
×
304
                }
305

306
                return $server_info;
×
307
        }
308

309
        /**
310
         * Returns info about the Yoast SEO plugin version and license.
311
         *
312
         * @param object $plugin The plugin.
313
         *
314
         * @return string The product info.
315
         */
316
        private function get_product_info( $plugin ) {
×
317
                if ( empty( $plugin ) ) {
×
318
                        return '';
×
319
                }
320

321
                $product_info = \sprintf(
×
322
                        'Expiration date %1$s',
×
323
                        $plugin->expiry_date,
×
324
                );
×
325

326
                return $product_info;
×
327
        }
328

329
        /**
330
         * Returns the WordPress version + a suffix about the multisite status.
331
         *
332
         * @return string The WordPress version string.
333
         */
334
        private function get_wordpress_version() {
×
335
                global $wp_version;
×
336

337
                $wordpress_version = $wp_version;
×
338
                if ( \is_multisite() ) {
×
339
                        $wordpress_version .= ' (multisite: yes)';
×
340
                }
341
                else {
342
                        $wordpress_version .= ' (multisite: no)';
×
343
                }
344

345
                return $wordpress_version;
×
346
        }
347

348
        /**
349
         * Returns information about the current theme.
350
         *
351
         * @return string The theme info as string.
352
         */
353
        private function get_theme_info() {
×
354
                $theme = \wp_get_theme();
×
355

356
                $theme_info = \sprintf(
×
357
                        '%1$s (Version %2$s, %3$s)',
×
358
                        \esc_html( $theme->display( 'Name' ) ),
×
359
                        \esc_html( $theme->display( 'Version' ) ),
×
360
                        \esc_attr( $theme->display( 'ThemeURI' ) ),
×
361
                );
×
362

363
                if ( \is_child_theme() ) {
×
364
                        $theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
×
365
                }
366

367
                return $theme_info;
×
368
        }
369

370
        /**
371
         * Returns a stringified list of all active plugins, separated by a pipe.
372
         *
373
         * @return string The active plugins.
374
         */
375
        private function get_active_plugins() {
×
376
                $updates_available = \get_site_transient( 'update_plugins' );
×
377

378
                $active_plugins = '';
×
379
                foreach ( \wp_get_active_and_valid_plugins() as $plugin ) {
×
380
                        $plugin_data             = \get_plugin_data( $plugin );
×
381
                        $plugin_file             = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
×
382
                        $plugin_update_available = '';
×
383

384
                        if ( isset( $updates_available->response[ $plugin_file ] ) ) {
×
385
                                $plugin_update_available = ' [update available]';
×
386
                        }
387

388
                        $active_plugins .= \sprintf(
×
389
                                '%1$s (Version %2$s%3$s, %4$s) | ',
×
390
                                \esc_html( $plugin_data['Name'] ),
×
391
                                \esc_html( $plugin_data['Version'] ),
×
392
                                $plugin_update_available,
×
393
                                \esc_attr( $plugin_data['PluginURI'] ),
×
394
                        );
×
395
                }
396

397
                return $active_plugins;
×
398
        }
399

400
        /**
401
         * Returns a CSV list of all must-use and drop-in plugins.
402
         *
403
         * @return string The active plugins.
404
         */
405
        private function get_mustuse_and_dropins() {
×
406
                $dropins         = \get_dropins();
×
407
                $mustuse_plugins = \get_mu_plugins();
×
408

409
                if ( ! \is_array( $dropins ) ) {
×
410
                        $dropins = [];
×
411
                }
412

413
                if ( ! \is_array( $mustuse_plugins ) ) {
×
414
                        $mustuse_plugins = [];
×
415
                }
416

417
                return \sprintf( 'Must-Use plugins: %1$d, Drop-ins: %2$d', \count( $mustuse_plugins ), \count( $dropins ) );
×
418
        }
419

420
        /**
421
         * Return the indexables status details.
422
         *
423
         * @return string The indexables status in a string.
424
         */
425
        private function get_indexables_status() {
×
426
                $indexables_status  = 'Indexing completed: ';
×
427
                $indexing_completed = $this->options->get( 'indexables_indexing_completed' );
×
428
                $indexing_reason    = $this->options->get( 'indexing_reason' );
×
429

430
                $indexables_status .= ( $indexing_completed ) ? 'yes' : 'no';
×
431
                $indexables_status .= ( $indexing_reason ) ? ', latest indexing reason: ' . \esc_html( $indexing_reason ) : '';
×
432

433
                foreach ( [ 'free', 'premium' ] as $migration_name ) {
×
434
                        $current_status = $this->migration_status->get_error( $migration_name );
×
435

436
                        if ( \is_array( $current_status ) && isset( $current_status['message'] ) ) {
×
437
                                $indexables_status .= ', migration error: ' . \esc_html( $current_status['message'] );
×
438
                        }
439
                }
440

441
                return $indexables_status;
×
442
        }
443

444
        /**
445
         * Returns language settings for the website and the current user.
446
         *
447
         * @return string The locale settings of the site and user.
448
         */
449
        private function get_language_settings() {
×
450
                $site_locale = \get_locale();
×
451
                $user_locale = \get_user_locale();
×
452

453
                $language_settings = \sprintf(
×
454
                        'Site locale: %1$s, user locale: %2$s',
×
455
                        ( \is_string( $site_locale ) ) ? \esc_html( $site_locale ) : 'unknown',
×
456
                        ( \is_string( $user_locale ) ) ? \esc_html( $user_locale ) : 'unknown',
×
457
                );
×
458

459
                return $language_settings;
×
460
        }
461

462
        /**
463
         * Returns the conditionals based on which this integration should be active.
464
         *
465
         * @return array<string> The array of conditionals.
466
         */
467
        public static function get_conditionals() {
×
NEW
468
                return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_Conditional::class ];
×
469
        }
470

471
        /**
472
         * Get the beacon id to use based on the user's subscription and tracking settings.
473
         *
474
         * @return string The beacon id to use.
475
         */
476
        private function get_beacon_id() {
×
477
                // Case where the user has a Yoast WooCommerce SEO plan subscription (highest priority).
478
                if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) {
×
479
                        return $this->beacon_id_woocommerce;
×
480
                }
481

482
                // Case where the user has a Yoast SEO Premium plan subscription.
483
                if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
×
484
                        return $this->beacon_id_premium;
×
485
                }
486

487
                // Case where the user has no plan active and tracking enabled.
488
                if ( $this->ask_consent ) {
×
489
                        return $this->beacon_id_tracking_users;
×
490
                }
491

492
                // Case where the user has no plan active and tracking disabled.
493
                return $this->beacon_id;
×
494
        }
495

496
        /**
497
         * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late.
498
         *
499
         * @return void
500
         */
501
        protected function filter_settings() {
×
502
                $filterable_helpscout_setting = [
×
503
                        'products'  => $this->products,
×
504
                        'pages_ids' => $this->pages_ids,
×
505
                ];
×
506

507
                /**
508
                 * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
509
                 *
510
                 * @param string $beacon_settings The HelpScout beacon settings.
511
                 */
512
                $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );
×
513
                $this->products     = $helpscout_settings['products'];
×
514
                $this->pages_ids    = $helpscout_settings['pages_ids'];
×
515
        }
516
}
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