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

Yoast / wordpress-seo / d5c0395617268a4e44627c6847bf3faa0a6cc695

15 Apr 2025 07:12AM UTC coverage: 52.454% (-2.1%) from 54.595%
d5c0395617268a4e44627c6847bf3faa0a6cc695

push

github

web-flow
Merge pull request #22077 from Yoast/feature/drop-php-7.2-7.3

Drop compatibility with PHP 7.2 and 7.3

7826 of 13877 branches covered (56.4%)

Branch coverage included in aggregate %.

29028 of 56382 relevant lines covered (51.48%)

42273.52 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\Config\Migration_Status;
11
use Yoast\WP\SEO\Helpers\Options_Helper;
12
use Yoast\WP\SEO\Integrations\Academy_Integration;
13
use Yoast\WP\SEO\Integrations\Integration_Interface;
14
use Yoast\WP\SEO\Integrations\Settings_Integration;
15
use Yoast\WP\SEO\Integrations\Support_Integration;
16

17
/**
18
 * Class WPSEO_HelpScout
19
 */
20
class HelpScout_Beacon implements Integration_Interface {
21

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

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

36
        /**
37
         * The products the beacon is loaded for.
38
         *
39
         * @var array
40
         */
41
        protected $products = [];
42

43
        /**
44
         * Whether to ask the user's consent before loading in HelpScout.
45
         *
46
         * @var bool
47
         */
48
        protected $ask_consent = true;
49

50
        /**
51
         * The options helper.
52
         *
53
         * @var Options_Helper
54
         */
55
        protected $options;
56

57
        /**
58
         * The array of pages we need to show the beacon on with their respective beacon IDs.
59
         *
60
         * @var array
61
         */
62
        protected $pages_ids;
63

64
        /**
65
         * The array of pages we need to show the beacon on.
66
         *
67
         * @var array
68
         */
69
        protected $base_pages = [
70
                'wpseo_dashboard',
71
                Settings_Integration::PAGE,
72
                Academy_Integration::PAGE,
73
                Support_Integration::PAGE,
74
                'wpseo_search_console',
75
                'wpseo_tools',
76
                'wpseo_licenses',
77
                'wpseo_workouts',
78
                'wpseo_integrations',
79
        ];
80

81
        /**
82
         * The current admin page
83
         *
84
         * @var string|null
85
         */
86
        protected $page;
87

88
        /**
89
         * The asset manager.
90
         *
91
         * @var WPSEO_Admin_Asset_Manager
92
         */
93
        protected $asset_manager;
94

95
        /**
96
         * The migration status object.
97
         *
98
         * @var Migration_Status
99
         */
100
        protected $migration_status;
101

102
        /**
103
         * Headless_Rest_Endpoints_Enabled_Conditional constructor.
104
         *
105
         * @param Options_Helper            $options          The options helper.
106
         * @param WPSEO_Admin_Asset_Manager $asset_manager    The asset manager.
107
         * @param Migration_Status          $migration_status The migrations status.
108
         */
109
        public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status ) {
×
110
                $this->options       = $options;
×
111
                $this->asset_manager = $asset_manager;
×
112
                $this->ask_consent   = ! $this->options->get( 'tracking' );
×
113
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
114
                if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) {
×
115
                        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
116
                        $this->page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) );
×
117
                }
118
                else {
119
                        $this->page = null;
×
120
                }
121
                $this->migration_status = $migration_status;
×
122

123
                foreach ( $this->base_pages as $page ) {
×
124
                        if ( $this->ask_consent ) {
×
125
                                // We want to be able to show surveys to people who have tracking on, so we give them a different beacon.
126
                                $this->pages_ids[ $page ] = $this->beacon_id_tracking_users;
×
127
                        }
128
                        else {
129
                                $this->pages_ids[ $page ] = $this->beacon_id;
×
130
                        }
131
                }
132
        }
133

134
        /**
135
         * {@inheritDoc}
136
         */
137
        public function register_hooks() {
×
138
                \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] );
×
139
                \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] );
×
140
        }
141

142
        /**
143
         * Enqueues the HelpScout script.
144
         *
145
         * @return void
146
         */
147
        public function enqueue_help_scout_script() {
×
148
                // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon.
149
                $this->filter_settings();
×
150
                if ( ! $this->is_beacon_page() ) {
×
151
                        return;
×
152
                }
153

154
                $this->asset_manager->enqueue_script( 'help-scout-beacon' );
×
155
        }
156

157
        /**
158
         * Outputs a small piece of javascript for the beacon.
159
         *
160
         * @return void
161
         */
162
        public function output_beacon_js() {
×
163
                if ( ! $this->is_beacon_page() ) {
×
164
                        return;
×
165
                }
166

167
                \printf(
×
168
                        '<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>',
×
169
                        ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon',
×
170
                        \esc_html( $this->pages_ids[ $this->page ] ),
×
171
                        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode.
172
                        WPSEO_Utils::format_json_encode( (array) $this->get_session_data() )
×
173
                );
×
174
        }
175

176
        /**
177
         * Checks if the current page is a page containing the beacon.
178
         *
179
         * @return bool
180
         */
181
        private function is_beacon_page() {
×
182
                $return = false;
×
183
                if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) {
×
184
                        $return = true;
×
185
                }
186

187
                /**
188
                 * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon.
189
                 *
190
                 * @param bool $show_beacon Whether we show the beacon or not.
191
                 */
192
                return \apply_filters( 'wpseo_helpscout_show_beacon', $return );
×
193
        }
194

195
        /**
196
         * Retrieves the identifying data.
197
         *
198
         * @return string The data to pass as identifying data.
199
         */
200
        protected function get_session_data() {
×
201
                // Short-circuit if we can get the needed data from a transient.
202
                $transient_data = \get_transient( 'yoast_beacon_session_data' );
×
203

204
                if ( \is_array( $transient_data ) ) {
×
205
                        return WPSEO_Utils::format_json_encode( $transient_data );
×
206
                }
207

208
                $current_user = \wp_get_current_user();
×
209

210
                // Do not make these strings translatable! They are for our support agents, the user won't see them!
211
                $data = \array_merge(
×
212
                        [
×
213
                                'name'               => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ),
×
214
                                'email'              => $current_user->user_email,
×
215
                                'Languages'          => $this->get_language_settings(),
×
216
                        ],
×
217
                        $this->get_server_info(),
×
218
                        [
×
219
                                'WordPress Version'    => $this->get_wordpress_version(),
×
220
                                'Active theme'         => $this->get_theme_info(),
×
221
                                'Active plugins'       => $this->get_active_plugins(),
×
222
                                'Must-use and dropins' => $this->get_mustuse_and_dropins(),
×
223
                                'Indexables status'    => $this->get_indexables_status(),
×
224
                        ]
×
225
                );
×
226

227
                if ( ! empty( $this->products ) ) {
×
228
                        $addon_manager = new WPSEO_Addon_Manager();
×
229
                        foreach ( $this->products as $product ) {
×
230
                                $subscription = $addon_manager->get_subscription( $product );
×
231

232
                                if ( ! $subscription ) {
×
233
                                        continue;
×
234
                                }
235

236
                                $data[ $subscription->product->name ] = $this->get_product_info( $subscription );
×
237
                        }
238
                }
239

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

243
                return WPSEO_Utils::format_json_encode( $data );
×
244
        }
245

246
        /**
247
         * Returns basic info about the server software.
248
         *
249
         * @return array
250
         */
251
        private function get_server_info() {
×
252
                $server_tracking_data = new WPSEO_Tracking_Server_Data();
×
253
                $server_data          = $server_tracking_data->get();
×
254
                $server_data          = $server_data['server'];
×
255

256
                $fields_to_use = [
×
257
                        'Server IP'        => 'ip',
×
258
                        'PHP Version'      => 'PhpVersion',
×
259
                        'cURL Version'     => 'CurlVersion',
×
260
                ];
×
261

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

264
                $server_info = [];
×
265

266
                foreach ( $fields_to_use as $label => $field_to_use ) {
×
267
                        if ( isset( $server_data[ $field_to_use ] ) ) {
×
268
                                $server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] );
×
269
                        }
270
                }
271

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

276
                if ( $memory_limit !== \WP_MEMORY_LIMIT ) {
×
277
                        $server_info['Memory limits'] .= ', WP_MEMORY_LIMIT: ' . \WP_MEMORY_LIMIT;
×
278
                }
279

280
                if ( $memory_limit !== \WP_MAX_MEMORY_LIMIT ) {
×
281
                        $server_info['Memory limits'] .= ', WP_MAX_MEMORY_LIMIT: ' . \WP_MAX_MEMORY_LIMIT;
×
282
                }
283

284
                return $server_info;
×
285
        }
286

287
        /**
288
         * Returns info about the Yoast SEO plugin version and license.
289
         *
290
         * @param object $plugin The plugin.
291
         *
292
         * @return string The product info.
293
         */
294
        private function get_product_info( $plugin ) {
×
295
                if ( empty( $plugin ) ) {
×
296
                        return '';
×
297
                }
298

299
                $product_info = \sprintf(
×
300
                        'Expiration date %1$s',
×
301
                        $plugin->expiry_date
×
302
                );
×
303

304
                return $product_info;
×
305
        }
306

307
        /**
308
         * Returns the WordPress version + a suffix about the multisite status.
309
         *
310
         * @return string The WordPress version string.
311
         */
312
        private function get_wordpress_version() {
×
313
                global $wp_version;
×
314

315
                $wordpress_version = $wp_version;
×
316
                if ( \is_multisite() ) {
×
317
                        $wordpress_version .= ' (multisite: yes)';
×
318
                }
319
                else {
320
                        $wordpress_version .= ' (multisite: no)';
×
321
                }
322

323
                return $wordpress_version;
×
324
        }
325

326
        /**
327
         * Returns information about the current theme.
328
         *
329
         * @return string The theme info as string.
330
         */
331
        private function get_theme_info() {
×
332
                $theme = \wp_get_theme();
×
333

334
                $theme_info = \sprintf(
×
335
                        '%1$s (Version %2$s, %3$s)',
×
336
                        \esc_html( $theme->display( 'Name' ) ),
×
337
                        \esc_html( $theme->display( 'Version' ) ),
×
338
                        \esc_attr( $theme->display( 'ThemeURI' ) )
×
339
                );
×
340

341
                if ( \is_child_theme() ) {
×
342
                        $theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
×
343
                }
344

345
                return $theme_info;
×
346
        }
347

348
        /**
349
         * Returns a stringified list of all active plugins, separated by a pipe.
350
         *
351
         * @return string The active plugins.
352
         */
353
        private function get_active_plugins() {
×
354
                $updates_available = \get_site_transient( 'update_plugins' );
×
355

356
                $active_plugins = '';
×
357
                foreach ( \wp_get_active_and_valid_plugins() as $plugin ) {
×
358
                        $plugin_data             = \get_plugin_data( $plugin );
×
359
                        $plugin_file             = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
×
360
                        $plugin_update_available = '';
×
361

362
                        if ( isset( $updates_available->response[ $plugin_file ] ) ) {
×
363
                                $plugin_update_available = ' [update available]';
×
364
                        }
365

366
                        $active_plugins .= \sprintf(
×
367
                                '%1$s (Version %2$s%3$s, %4$s) | ',
×
368
                                \esc_html( $plugin_data['Name'] ),
×
369
                                \esc_html( $plugin_data['Version'] ),
×
370
                                $plugin_update_available,
×
371
                                \esc_attr( $plugin_data['PluginURI'] )
×
372
                        );
×
373
                }
374

375
                return $active_plugins;
×
376
        }
377

378
        /**
379
         * Returns a CSV list of all must-use and drop-in plugins.
380
         *
381
         * @return string The active plugins.
382
         */
383
        private function get_mustuse_and_dropins() {
×
384
                $dropins         = \get_dropins();
×
385
                $mustuse_plugins = \get_mu_plugins();
×
386

387
                if ( ! \is_array( $dropins ) ) {
×
388
                        $dropins = [];
×
389
                }
390

391
                if ( ! \is_array( $mustuse_plugins ) ) {
×
392
                        $mustuse_plugins = [];
×
393
                }
394

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

398
        /**
399
         * Return the indexables status details.
400
         *
401
         * @return string The indexables status in a string.
402
         */
403
        private function get_indexables_status() {
×
404
                $indexables_status  = 'Indexing completed: ';
×
405
                $indexing_completed = $this->options->get( 'indexables_indexing_completed' );
×
406
                $indexing_reason    = $this->options->get( 'indexing_reason' );
×
407

408
                $indexables_status .= ( $indexing_completed ) ? 'yes' : 'no';
×
409
                $indexables_status .= ( $indexing_reason ) ? ', latest indexing reason: ' . \esc_html( $indexing_reason ) : '';
×
410

411
                foreach ( [ 'free', 'premium' ] as $migration_name ) {
×
412
                        $current_status = $this->migration_status->get_error( $migration_name );
×
413

414
                        if ( \is_array( $current_status ) && isset( $current_status['message'] ) ) {
×
415
                                $indexables_status .= ', migration error: ' . \esc_html( $current_status['message'] );
×
416
                        }
417
                }
418

419
                return $indexables_status;
×
420
        }
421

422
        /**
423
         * Returns language settings for the website and the current user.
424
         *
425
         * @return string The locale settings of the site and user.
426
         */
427
        private function get_language_settings() {
×
428
                $site_locale = \get_locale();
×
429
                $user_locale = \get_user_locale();
×
430

431
                $language_settings = \sprintf(
×
432
                        'Site locale: %1$s, user locale: %2$s',
×
433
                        ( \is_string( $site_locale ) ) ? \esc_html( $site_locale ) : 'unknown',
×
434
                        ( \is_string( $user_locale ) ) ? \esc_html( $user_locale ) : 'unknown'
×
435
                );
×
436

437
                return $language_settings;
×
438
        }
439

440
        /**
441
         * Returns the conditionals based on which this integration should be active.
442
         *
443
         * @return array The array of conditionals.
444
         */
445
        public static function get_conditionals() {
×
446
                return [ Admin_Conditional::class ];
×
447
        }
448

449
        /**
450
         * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late.
451
         *
452
         * @return void
453
         */
454
        protected function filter_settings() {
×
455
                $filterable_helpscout_setting = [
×
456
                        'products'  => $this->products,
×
457
                        'pages_ids' => $this->pages_ids,
×
458
                ];
×
459

460
                /**
461
                 * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
462
                 *
463
                 * @param string $beacon_settings The HelpScout beacon settings.
464
                 */
465
                $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );
×
466

467
                $this->products  = $helpscout_settings['products'];
×
468
                $this->pages_ids = $helpscout_settings['pages_ids'];
×
469
        }
470
}
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