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

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