PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / integrations / admin / helpscout-beacon.php

helpscout-beacon.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.9, at src/integrations/admin/helpscout-beacon.php

517 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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() {
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 }
517