PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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
← All changes | src/integrations/admin/helpscout-beacon.php +234 -67 18.2 → trunk View file →
@@ -5,11 +5,18 @@
5 5 use WPSEO_Addon_Manager;
6 6 use WPSEO_Admin_Asset_Manager;
7 7 use WPSEO_Tracking_Server_Data;
8 8 use WPSEO_Utils;
9 +use Yoast\WP\SEO\Bulk_Editor\User_Interface\Bulk_Editor_Integration;
9 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;
10 13 use Yoast\WP\SEO\Helpers\Options_Helper;
14 +use Yoast\WP\SEO\Integrations\Academy_Integration;
11 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;
12 19
13 20 /**
14 21 * Class WPSEO_HelpScout
15 22 */
@@ -29,16 +36,30 @@
29 36 */
30 37 protected $beacon_id_tracking_users = '6b8e74c5-aa81-4295-b97b-c2a62a13ea7f';
31 38
32 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 + /**
33 54 * The products the beacon is loaded for.
34 55 *
35 - * @var array
56 + * @var array<string>
36 57 */
37 58 protected $products = [];
38 59
39 60 /**
40 - * Whether to asks the user's consent before loading in HelpScout.
61 + * Whether to ask the user's consent before loading in HelpScout.
41 62 *
42 63 * @var bool
43 64 */
44 65 protected $ask_consent = true;
@@ -50,11 +71,18 @@
50 71 */
51 72 protected $options;
52 73
53 74 /**
75 + * The addon manager.
76 + *
77 + * @var WPSEO_Addon_Manager
78 + */
79 + protected $addon_manager;
80 +
81 + /**
54 82 * The array of pages we need to show the beacon on with their respective beacon IDs.
55 83 *
56 - * @var array
84 + * @var array<string, string>
57 85 */
58 86 protected $pages_ids;
59 87
60 88 /**
@@ -59,24 +87,27 @@
59 87
60 88 /**
61 89 * The array of pages we need to show the beacon on.
62 90 *
63 - * @var array
91 + * @var array<string>
64 92 */
65 93 protected $base_pages = [
66 94 'wpseo_dashboard',
67 - 'wpseo_titles',
95 + Settings_Integration::PAGE,
96 + Academy_Integration::PAGE,
97 + Support_Integration::PAGE,
68 98 'wpseo_search_console',
69 - 'wpseo_social',
70 99 'wpseo_tools',
71 - 'wpseo_licenses',
100 + Plans_Page_Integration::PAGE,
72 101 'wpseo_workouts',
102 + Integrations_Page::PAGE,
103 + Bulk_Editor_Integration::PAGE,
73 104 ];
74 105
75 106 /**
76 107 * The current admin page
77 108 *
78 - * @var string
109 + * @var string|null
79 110 */
80 111 protected $page;
81 112
82 113 /**
@@ -86,32 +117,47 @@
86 117 */
87 118 protected $asset_manager;
88 119
89 120 /**
121 + * The migration status object.
122 + *
123 + * @var Migration_Status
124 + */
125 + protected $migration_status;
126 +
127 + /**
90 128 * Headless_Rest_Endpoints_Enabled_Conditional constructor.
91 129 *
92 - * @param Options_Helper $options The options helper.
93 - * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
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.
94 134 */
95 - public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager ) {
135 + public function __construct( Options_Helper $options, WPSEO_Admin_Asset_Manager $asset_manager, Migration_Status $migration_status, WPSEO_Addon_Manager $addon_manager ) {
96 136 $this->options = $options;
97 137 $this->asset_manager = $asset_manager;
138 + $this->addon_manager = $addon_manager;
98 139 $this->ask_consent = ! $this->options->get( 'tracking' );
99 - $this->page = \filter_input( \INPUT_GET, 'page', \FILTER_SANITIZE_STRING );
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;
100 149
150 + $beacon_id = $this->get_beacon_id();
101 151 foreach ( $this->base_pages as $page ) {
102 - if ( $this->ask_consent ) {
103 - // We want to be able to show surveys to people who have tracking on, so we give them a different beacon.
104 - $this->pages_ids[ $page ] = $this->beacon_id_tracking_users;
105 - }
106 - else {
107 - $this->pages_ids[ $page ] = $this->beacon_id;
108 - }
152 + $this->pages_ids[ $page ] = $beacon_id;
109 153 }
110 154 }
111 155
112 156 /**
113 157 * {@inheritDoc}
158 + *
159 + * @return void
114 160 */
115 161 public function register_hooks() {
116 162 \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_help_scout_script' ] );
117 163 \add_action( 'admin_footer', [ $this, 'output_beacon_js' ] );
@@ -118,8 +164,10 @@
118 164 }
119 165
120 166 /**
121 167 * Enqueues the HelpScout script.
168 + *
169 + * @return void
122 170 */
123 171 public function enqueue_help_scout_script() {
124 172 // Make sure plugins can filter in their "stuff", before we check whether we're outputting a beacon.
125 173 $this->filter_settings();
@@ -131,8 +179,10 @@
131 179 }
132 180
133 181 /**
134 182 * Outputs a small piece of javascript for the beacon.
183 + *
184 + * @return void
135 185 */
136 186 public function output_beacon_js() {
137 187 if ( ! $this->is_beacon_page() ) {
138 188 return;
@@ -142,9 +192,9 @@
142 192 '<script type="text/javascript">window.%1$s(\'%2$s\', %3$s)</script>',
143 193 ( $this->ask_consent ) ? 'wpseoHelpScoutBeaconConsent' : 'wpseoHelpScoutBeacon',
144 194 \esc_html( $this->pages_ids[ $this->page ] ),
145 195 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaping done in format_json_encode.
146 - WPSEO_Utils::format_json_encode( (array) $this->get_session_data() )
196 + WPSEO_Utils::format_json_encode( (array) $this->get_session_data() ),
147 197 );
148 198 }
149 199
150 200 /**
@@ -160,9 +210,9 @@
160 210
161 211 /**
162 212 * Filter: 'wpseo_helpscout_show_beacon' - Allows overriding whether we show the HelpScout beacon.
163 213 *
164 - * @api bool - Whether we show the beacon or not.
214 + * @param bool $show_beacon Whether we show the beacon or not.
165 215 */
166 216 return \apply_filters( 'wpseo_helpscout_show_beacon', $return );
167 217 }
168 218
@@ -171,19 +221,33 @@
171 221 *
172 222 * @return string The data to pass as identifying data.
173 223 */
174 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 +
175 232 $current_user = \wp_get_current_user();
176 233
177 234 // Do not make these strings translatable! They are for our support agents, the user won't see them!
178 - $data = [
179 - 'name' => \trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ),
180 - 'email' => $current_user->user_email,
181 - 'WordPress Version' => $this->get_wordpress_version(),
182 - 'Server' => $this->get_server_info(),
183 - '<a href="' . \admin_url( 'themes.php' ) . '">Theme</a>' => $this->get_theme_info(),
184 - '<a href="' . \admin_url( 'plugins.php' ) . '">Plugins</a>' => $this->get_active_plugins(),
185 - ];
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 + );
186 250
187 251 if ( ! empty( $this->products ) ) {
188 252 $addon_manager = new WPSEO_Addon_Manager();
189 253 foreach ( $this->products as $product ) {
@@ -196,8 +260,11 @@
196 260 $data[ $subscription->product->name ] = $this->get_product_info( $subscription );
197 261 }
198 262 }
199 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 +
200 267 return WPSEO_Utils::format_json_encode( $data );
201 268 }
202 269
203 270 /**
@@ -202,9 +269,9 @@
202 269
203 270 /**
204 271 * Returns basic info about the server software.
205 272 *
206 - * @return string
273 + * @return array<string, string>
207 274 */
208 275 private function get_server_info() {
209 276 $server_tracking_data = new WPSEO_Tracking_Server_Data();
210 277 $server_data = $server_tracking_data->get();
@@ -210,27 +277,35 @@
210 277 $server_data = $server_tracking_data->get();
211 278 $server_data = $server_data['server'];
212 279
213 280 $fields_to_use = [
214 - 'IP' => 'ip',
215 - 'Hostname' => 'Hostname',
216 - 'OS' => 'os',
217 - 'PHP' => 'PhpVersion',
218 - 'CURL' => 'CurlVersion',
281 + 'Server IP' => 'ip',
282 + 'PHP Version' => 'PhpVersion',
283 + 'cURL Version' => 'CurlVersion',
219 284 ];
220 285
221 - $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . '(SSL Support' . $server_data['CurlVersion']['sslSupport'] . ')';
286 + $server_data['CurlVersion'] = $server_data['CurlVersion']['version'] . ' (SSL Support ' . $server_data['CurlVersion']['sslSupport'] . ')';
222 287
223 - $server_info = '<table>';
288 + $server_info = [];
224 289
225 290 foreach ( $fields_to_use as $label => $field_to_use ) {
226 291 if ( isset( $server_data[ $field_to_use ] ) ) {
227 - $server_info .= \sprintf( '<tr><td>%1$s</td><td>%2$s</td></tr>', \esc_html( $label ), \esc_html( $server_data[ $field_to_use ] ) );
292 + $server_info[ $label ] = \esc_html( $server_data[ $field_to_use ] );
228 293 }
229 294 }
230 295
231 - $server_info .= '</table>';
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;
232 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 +
233 308 return $server_info;
234 309 }
235 310
236 311 /**
@@ -244,18 +319,18 @@
244 319 if ( empty( $plugin ) ) {
245 320 return '';
246 321 }
247 322
248 - $product_info = '<table>';
249 - $product_info .= '<tr><td>Version</td><td>' . $plugin->product->version . '</td></tr>';
250 - $product_info .= '<tr><td>Expiration date</td><td>' . $plugin->expiry_date . '</td></tr>';
251 - $product_info .= '</table>';
323 + $product_info = \sprintf(
324 + 'Expiration date %1$s',
325 + $plugin->expiry_date,
326 + );
252 327
253 328 return $product_info;
254 329 }
255 330
256 331 /**
257 - * Returns the WordPress version + a suffix if current WP is multi site.
332 + * Returns the WordPress version + a suffix about the multisite status.
258 333 *
259 334 * @return string The WordPress version string.
260 335 */
261 336 private function get_wordpress_version() {
@@ -262,16 +337,19 @@
262 337 global $wp_version;
263 338
264 339 $wordpress_version = $wp_version;
265 340 if ( \is_multisite() ) {
266 - $wordpress_version .= ' MULTI-SITE';
341 + $wordpress_version .= ' (multisite: yes)';
267 342 }
343 + else {
344 + $wordpress_version .= ' (multisite: no)';
345 + }
268 346
269 347 return $wordpress_version;
270 348 }
271 349
272 350 /**
273 - * Returns a formatted HTML string for the current theme.
351 + * Returns information about the current theme.
274 352 *
275 353 * @return string The theme info as string.
276 354 */
277 355 private function get_theme_info() {
@@ -277,17 +355,16 @@
277 355 private function get_theme_info() {
278 356 $theme = \wp_get_theme();
279 357
280 358 $theme_info = \sprintf(
281 - '<a href="%1$s">%2$s</a> v%3$s by %4$s',
282 - \esc_attr( $theme->display( 'ThemeURI' ) ),
359 + '%1$s (Version %2$s, %3$s)',
283 360 \esc_html( $theme->display( 'Name' ) ),
284 361 \esc_html( $theme->display( 'Version' ) ),
285 - \esc_html( $theme->display( 'Author' ) )
362 + \esc_attr( $theme->display( 'ThemeURI' ) ),
286 363 );
287 364
288 365 if ( \is_child_theme() ) {
289 - $theme_info .= \sprintf( '<br />Child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
366 + $theme_info .= \sprintf( ', this is a child theme of: %1$s', \esc_html( $theme->display( 'Template' ) ) );
290 367 }
291 368
292 369 return $theme_info;
293 370 }
@@ -292,9 +369,9 @@
292 369 return $theme_info;
293 370 }
294 371
295 372 /**
296 - * Returns a formatted HTML list of all active plugins.
373 + * Returns a stringified list of all active plugins, separated by a pipe.
297 374 *
298 375 * @return string The active plugins.
299 376 */
300 377 private function get_active_plugins() {
@@ -301,20 +378,22 @@
301 378 $updates_available = \get_site_transient( 'update_plugins' );
302 379
303 380 $active_plugins = '';
304 381 foreach ( \wp_get_active_and_valid_plugins() as $plugin ) {
305 - $plugin_data = \get_plugin_data( $plugin );
306 - $plugin_file = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
382 + $plugin_data = \get_plugin_data( $plugin );
383 + $plugin_file = \str_replace( \trailingslashit( \WP_PLUGIN_DIR ), '', $plugin );
384 + $plugin_update_available = '';
307 385
308 386 if ( isset( $updates_available->response[ $plugin_file ] ) ) {
309 - $active_plugins .= '<i class="icon-close1"></i> ';
387 + $plugin_update_available = ' [update available]';
310 388 }
311 389
312 390 $active_plugins .= \sprintf(
313 - '<a href="%1$s">%2$s</a> v%3$s',
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,
314 395 \esc_attr( $plugin_data['PluginURI'] ),
315 - \esc_html( $plugin_data['Name'] ),
316 - \esc_html( $plugin_data['Version'] )
317 396 );
318 397 }
319 398
320 399 return $active_plugins;
@@ -320,32 +399,120 @@
320 399 return $active_plugins;
321 400 }
322 401
323 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 + /**
324 465 * Returns the conditionals based on which this integration should be active.
325 466 *
326 - * @return array The array of conditionals.
467 + * @return array<string> The array of conditionals.
327 468 */
328 469 public static function get_conditionals() {
329 - return [ Admin_Conditional::class ];
470 + return [ Admin_Conditional::class, User_Can_Manage_Wpseo_Options_Conditional::class ];
330 471 }
331 472
332 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 + /**
333 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
334 502 */
335 503 protected function filter_settings() {
336 - /**
337 - * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
338 - *
339 - * @api string - The HelpScout beacon settings.
340 - */
341 504 $filterable_helpscout_setting = [
342 505 'products' => $this->products,
343 506 'pages_ids' => $this->pages_ids,
344 507 ];
345 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 + */
346 514 $helpscout_settings = \apply_filters( 'wpseo_helpscout_beacon_settings', $filterable_helpscout_setting );
347 -
348 - $this->products = $helpscout_settings['products'];
349 - $this->pages_ids = $helpscout_settings['pages_ids'];
515 + $this->products = $helpscout_settings['products'];
516 + $this->pages_ids = $helpscout_settings['pages_ids'];
350 517 }
351 518 }