PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.5
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.5, at src/integrations/admin/helpscout-beacon.php

516 lines 14.2 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\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 use Yoast\WP\SEO\Plans\User_Interface\Plans_Page_Integration;
17
18 /**
19 * Class WPSEO_HelpScout
20 */
21 class HelpScout_Beacon implements Integration_Interface {
22
23 /**
24 * The id for the beacon.
25 *
26 * @var string
27 */
28 protected $beacon_id = '2496aba6-0292-489c-8f5d-1c0fba417c2f';
29
30 /**
31 * The id for the beacon for users that have tracking on.
32 *
33 * @var string
34 */
35 protected $beacon_id_tracking_users = '6b8e74c5-aa81-4295-b97b-c2a62a13ea7f';
36
37 /**
38 * The id for the beacon for Premium users.
39 *
40 * @var string
41 */
42 protected $beacon_id_premium = '1ae02e91-5865-4f13-b220-7daed946ba25';
43
44 /**
45 * The id for the beacon for WooCommerce SEO users.
46 *
47 * @var string
48 */
49 protected $beacon_id_woocommerce = '8535d745-4e80-48b9-b211-087880aa857d';
50
51 /**
52 * The products the beacon is loaded for.
53 *
54 * @var array<string>
55 */
56 protected $products = [];
57
58 /**
59 * Whether to ask the user's consent before loading in HelpScout.
60 *
61 * @var bool
62 */
63 protected $ask_consent = true;
64
65 /**
66 * The options helper.
67 *
68 * @var Options_Helper
69 */
70 protected $options;
71
72 /**
73 * The addon manager.
74 *
75 * @var WPSEO_Addon_Manager
76 */
77 protected $addon_manager;
78
79 /**
80 * The array of pages we need to show the beacon on with their respective beacon IDs.
81 *
82 * @var array<string, string>
83 */
84 protected $pages_ids;
85
86 /**
87 * The array of pages we need to show the beacon on.
88 *
89 * @var array<string>
90 */
91 protected $base_pages = [
92 'wpseo_dashboard',
93 Settings_Integration::PAGE,
94 Academy_Integration::PAGE,
95 Support_Integration::PAGE,
96 'wpseo_search_console',
97 'wpseo_tools',
98 Plans_Page_Integration::PAGE,
99 'wpseo_workouts',
100 'wpseo_integrations',
101 ];
102
103 /**
104 * The current admin page
105 *
106 * @var string|null
107 */
108 protected $page;
109
110 /**
111 * The asset manager.
112 *
113 * @var WPSEO_Admin_Asset_Manager
114 */
115 protected $asset_manager;
116
117 /**
118 * The migration status object.
119 *
120 * @var Migration_Status
121 */
122 protected $migration_status;
123
124 /**
125 * Headless_Rest_Endpoints_Enabled_Conditional constructor.
126 *
127 * @param Options_Helper $options The options helper.
128 * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
129 * @param Migration_Status $migration_status The migrations status.
130 * @param WPSEO_Addon_Manager $addon_manager The addon manager.
131 */
132 public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status, WPSEO_Addon_Manager $addon_manager ) {
133 $this->options = $options;
134 $this->asset_manager = $asset_manager;
135 $this->addon_manager = $addon_manager;
136 $this->ask_consent = ! $this->options->get( 'tracking' );
137 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
138 if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) {
139 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
140 $this->page = \sanitize_text_field( \wp_unslash( $_GET['page'] ) );
141 }
142 else {
143 $this->page = null;
144 }
145 $this->migration_status = $migration_status;
146
147 $beacon_id = $this->get_beacon_id();
148 foreach ( $this->base_pages as $page ) {
149 $this->pages_ids[ $page ] = $beacon_id;
150 }
151 }
152
153 /**
154 * {@inheritDoc}
155 *
156 * @return void
157 */
158 public function register_hooks() {
159 \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] );
160 \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] );
161 }
162
163 /**
164 * Enqueues the HelpScout script.
165 *
166 * @return void
167 */
168 public function enqueue_help_scout_script() {
169 // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon.
170 $this->filter_settings();
171 if ( ! $this->is_beacon_page() ) {
172 return;
173 }
174
175 $this->asset_manager->enqueue_script( 'help-scout-beacon' );
176 }
177
178 /**
179 * Outputs a small piece of javascript for the beacon.
180 *
181 * @return void
182 */
183 public function output_beacon_js() {
184 if ( ! $this->is_beacon_page() ) {
185 return;
186 }
187
188 \printf(
189 '<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>',
190 ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon',
191 \esc_html( $this->pages_ids[ $this->page ] ),
192 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode.
193 WPSEO_Utils::format_json_encode( (array) $this->get_session_data() ),
194 );
195 }
196
197 /**
198 * Checks if the current page is a page containing the beacon.
199 *
200 * @return bool
201 */
202 private function is_beacon_page() {
203 $return = false;
204 if ( ! empty( $this->page ) && $GLOBALS['pagenow'] === 'admin.php' && isset( $this->pages_ids[ $this->page ] ) ) {
205 $return = true;
206 }
207
208 /**
209 * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon.
210 *
211 * @param bool $show_beacon Whether we show the beacon or not.
212 */
213 return \apply_filters( 'wpseo_helpscout_show_beacon', $return );
214 }
215
216 /**
217 * Retrieves the identifying data.
218 *
219 * @return string The data to pass as identifying data.
220 */
221 protected function get_session_data() {
222 // Short-circuit if we can get the needed data from a transient.
223 $transient_data = \get_transient( 'yoast_beacon_session_data' );
224
225 if ( \is_array( $transient_data ) ) {
226 return WPSEO_Utils::format_json_encode( $transient_data );
227 }
228
229 $current_user = \wp_get_current_user();
230
231 // Do not make these strings translatable! They are for our support agents, the user won't see them!
232 $data = \array_merge(
233 [
234 'name' => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ),
235 'email' => $current_user->user_email,
236 'Languages' => $this->get_language_settings(),
237 ],
238 $this->get_server_info(),
239 [
240 'WordPress Version' => $this->get_wordpress_version(),
241 'Active theme' => $this->get_theme_info(),
242 'Active plugins' => $this->get_active_plugins(),
243 'Must-use and dropins' => $this->get_mustuse_and_dropins(),
244 'Indexables status' => $this->get_indexables_status(),
245 ],
246 );
247
248 if ( ! empty( $this->products ) ) {
249 $addon_manager = new WPSEO_Addon_Manager();
250 foreach ( $this->products as $product ) {
251 $subscription = $addon_manager->get_subscription( $product );
252
253 if ( ! $subscription ) {
254 continue;
255 }
256
257 $data[ $subscription->product->name ] = $this->get_product_info( $subscription );
258 }
259 }
260
261 // Store the data in a transient for 5 minutes to prevent overhead on every backend pageload.
262 \set_transient( 'yoast_beacon_session_data', $data, ( 5 * \MINUTE_IN_SECONDS ) );
263
264 return WPSEO_Utils::format_json_encode( $data );
265 }
266
267 /**
268 * Returns basic info about the server software.
269 *
270 * @return array<string, string>
271 */
272 private function get_server_info() {
273 $server_tracking_data = new WPSEO_Tracking_Server_Data();
274 $server_data = $server_tracking_data->get();
275 $server_data = $server_data['server'];
276
277 $fields_to_use = [
278 'Server IP' => 'ip',
279 'PHP Version' => 'PhpVersion',
280 'cURL Version' => 'CurlVersion',
281 ];
282
283 $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . ' (SSL Support ' . $server_data['CurlVersion']['sslSupport'] . ')';
284
285 $server_info = [];
286
287 foreach ( $fields_to_use as $label => $field_to_use ) {
288 if ( isset( $server_data[ $field_to_use ] ) ) {
289 $server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] );
290 }
291 }
292
293 // Get the memory limits for the server and, if different, from WordPress as well.
294 $memory_limit = \ini_get( 'memory_limit' );
295 $server_info['Memory limits'] = 'Server memory limit: ' . $memory_limit;
296
297 if ( $memory_limit !== \WP_MEMORY_LIMIT ) {
298 $server_info['Memory limits'] .= ', WP_MEMORY_LIMIT: ' . \WP_MEMORY_LIMIT;
299 }
300
301 if ( $memory_limit !== \WP_MAX_MEMORY_LIMIT ) {
302 $server_info['Memory limits'] .= ', WP_MAX_MEMORY_LIMIT: ' . \WP_MAX_MEMORY_LIMIT;
303 }
304
305 return $server_info;
306 }
307
308 /**
309 * Returns info about the Yoast SEO plugin version and license.
310 *
311 * @param object $plugin The plugin.
312 *
313 * @return string The product info.
314 */
315 private function get_product_info( $plugin ) {
316 if ( empty( $plugin ) ) {
317 return '';
318 }
319
320 $product_info = \sprintf(
321 'Expiration date %1$s',
322 $plugin->expiry_date,
323 );
324
325 return $product_info;
326 }
327
328 /**
329 * Returns the WordPress version + a suffix about the multisite status.
330 *
331 * @return string The WordPress version string.
332 */
333 private function get_wordpress_version() {
334 global $wp_version;
335
336 $wordpress_version = $wp_version;
337 if ( \is_multisite() ) {
338 $wordpress_version .= ' (multisite: yes)';
339 }
340 else {
341 $wordpress_version .= ' (multisite: no)';
342 }
343
344 return $wordpress_version;
345 }
346
347 /**
348 * Returns information about the current theme.
349 *
350 * @return string The theme info as string.
351 */
352 private function get_theme_info() {
353 $theme = \wp_get_theme();
354
355 $theme_info = \sprintf(
356 '%1$s (Version %2$s, %3$s)',
357 \esc_html( $theme->display( 'Name' ) ),
358 \esc_html( $theme->display( 'Version' ) ),
359 \esc_attr( $theme->display( 'ThemeURI' ) ),
360 );
361
362 if ( \is_child_theme() ) {
363 $theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
364 }
365
366 return $theme_info;
367 }
368
369 /**
370 * Returns a stringified list of all active plugins, separated by a pipe.
371 *
372 * @return string The active plugins.
373 */
374 private function get_active_plugins() {
375 $updates_available = \get_site_transient( 'update_plugins' );
376
377 $active_plugins = '';
378 foreach ( \wp_get_active_and_valid_plugins() as $plugin ) {
379 $plugin_data = \get_plugin_data( $plugin );
380 $plugin_file = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
381 $plugin_update_available = '';
382
383 if ( isset( $updates_available->response[ $plugin_file ] ) ) {
384 $plugin_update_available = ' [update available]';
385 }
386
387 $active_plugins .= \sprintf(
388 '%1$s (Version %2$s%3$s, %4$s) | ',
389 \esc_html( $plugin_data['Name'] ),
390 \esc_html( $plugin_data['Version'] ),
391 $plugin_update_available,
392 \esc_attr( $plugin_data['PluginURI'] ),
393 );
394 }
395
396 return $active_plugins;
397 }
398
399 /**
400 * Returns a CSV list of all must-use and drop-in plugins.
401 *
402 * @return string The active plugins.
403 */
404 private function get_mustuse_and_dropins() {
405 $dropins = \get_dropins();
406 $mustuse_plugins = \get_mu_plugins();
407
408 if ( ! \is_array( $dropins ) ) {
409 $dropins = [];
410 }
411
412 if ( ! \is_array( $mustuse_plugins ) ) {
413 $mustuse_plugins = [];
414 }
415
416 return \sprintf( 'Must-Use plugins: %1$d, Drop-ins: %2$d', \count( $mustuse_plugins ), \count( $dropins ) );
417 }
418
419 /**
420 * Return the indexables status details.
421 *
422 * @return string The indexables status in a string.
423 */
424 private function get_indexables_status() {
425 $indexables_status = 'Indexing completed: ';
426 $indexing_completed = $this->options->get( 'indexables_indexing_completed' );
427 $indexing_reason = $this->options->get( 'indexing_reason' );
428
429 $indexables_status .= ( $indexing_completed ) ? 'yes' : 'no';
430 $indexables_status .= ( $indexing_reason ) ? ', latest indexing reason: ' . \esc_html( $indexing_reason ) : '';
431
432 foreach ( [ 'free', 'premium' ] as $migration_name ) {
433 $current_status = $this->migration_status->get_error( $migration_name );
434
435 if ( \is_array( $current_status ) && isset( $current_status['message'] ) ) {
436 $indexables_status .= ', migration error: ' . \esc_html( $current_status['message'] );
437 }
438 }
439
440 return $indexables_status;
441 }
442
443 /**
444 * Returns language settings for the website and the current user.
445 *
446 * @return string The locale settings of the site and user.
447 */
448 private function get_language_settings() {
449 $site_locale = \get_locale();
450 $user_locale = \get_user_locale();
451
452 $language_settings = \sprintf(
453 'Site locale: %1$s, user locale: %2$s',
454 ( \is_string( $site_locale ) ) ? \esc_html( $site_locale ) : 'unknown',
455 ( \is_string( $user_locale ) ) ? \esc_html( $user_locale ) : 'unknown',
456 );
457
458 return $language_settings;
459 }
460
461 /**
462 * Returns the conditionals based on which this integration should be active.
463 *
464 * @return array<string> The array of conditionals.
465 */
466 public static function get_conditionals() {
467 return [ Admin_Conditional::class ];
468 }
469
470 /**
471 * Get the beacon id to use based on the user's subscription and tracking settings.
472 *
473 * @return string The beacon id to use.
474 */
475 private function get_beacon_id() {
476 // Case where the user has a Yoast WooCommerce SEO plan subscription (highest priority).
477 if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) {
478 return $this->beacon_id_woocommerce;
479 }
480
481 // Case where the user has a Yoast SEO Premium plan subscription.
482 if ( $this->addon_manager->has_active_addons() && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
483 return $this->beacon_id_premium;
484 }
485
486 // Case where the user has no plan active and tracking enabled.
487 if ( $this->ask_consent ) {
488 return $this->beacon_id_tracking_users;
489 }
490
491 // Case where the user has no plan active and tracking disabled.
492 return $this->beacon_id;
493 }
494
495 /**
496 * Allows filtering of the HelpScout settings. Hooked to admin_head to prevent timing issues, not too early, not too late.
497 *
498 * @return void
499 */
500 protected function filter_settings() {
501 $filterable_helpscout_setting = [
502 'products' => $this->products,
503 'pages_ids' => $this->pages_ids,
504 ];
505
506 /**
507 * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
508 *
509 * @param string $beacon_settings The HelpScout beacon settings.
510 */
511 $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );
512 $this->products = $helpscout_settings['products'];
513 $this->pages_ids = $helpscout_settings['pages_ids'];
514 }
515 }
516