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

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