Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.37% covered (danger)
19.37%
61 / 315
14.29% covered (danger)
14.29%
3 / 21
CRAP
n/a
0 / 0
wpcomsh_get_attachment_url
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_jetpack_sso_auth_cookie_expiration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_bypass_jetpack_sso_login
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
wpcomsh_modify_jetpack_sso_allowed_actions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_admin_enqueue_style
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_allow_custom_wp_options
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
check_site_has_pending_automated_transfer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_jetpack_api_fix_unserializable_track_number
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_filter_outgoing_user_agent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_allowed_redirect_hosts
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_make_content_clickable
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
6.68
wpcomsh_hide_scan_threats_from_transients
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_remove_threats_from_toolbar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_hide_scan_threats_from_api
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
wpcomsh_stats_timezone_string
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
wpcomsh_footer_rum_js
76.47% covered (warning)
76.47%
26 / 34
0.00% covered (danger)
0.00%
0 / 1
8.83
wpcomsh_get_woo_rum_data
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
wpcomsh_woocommerce_tracker_data
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_record_tracks_event
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
wpcomsh_jetpack_filter_tos_for_tracking
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_avoid_proxied_v2_banner
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Plugin Name: WordPress.com Site Helper
4 * Description: A helper for connecting WordPress.com sites to external host infrastructure.
5 * Version: 9.0.0
6 * Author: Automattic
7 * Author URI: http://automattic.com/
8 *
9 * @package wpcomsh
10 */
11
12define( 'WPCOMSH_VERSION', '9.0.0' );
13
14// Loaded first: fatal-error screen filter + one-shot plugin-deactivation endpoint.
15// The deactivator also needs to load before any regular plugin, so in production
16// a stub in wp-content/mu-plugins/ should re-include fatal-plugin-deactivator.php
17// directly (see wpcom-fatal-error/mu-plugin-stub.php).
18require_once __DIR__ . '/wpcom-fatal-error/load.php';
19
20// If true, Typekit fonts will be available in addition to Google fonts
21add_filter( 'jetpack_fonts_enable_typekit', '__return_true' );
22
23// This exists only on the Atomic platform. Blank if migrated elsewhere, so it doesn't fatal.
24if ( ! class_exists( 'Atomic_Persistent_Data' ) ) {
25    require_once __DIR__ . '/class-atomic-persistent-data.php';
26}
27
28require_once __DIR__ . '/constants.php';
29require_once __DIR__ . '/wpcom-features/functions-wpcom-features.php';
30require_once __DIR__ . '/wpcom-marketplace/software/class-marketplace-software-manager.php';
31require_once __DIR__ . '/functions.php';
32require_once __DIR__ . '/i18n.php';
33require_once __DIR__ . '/lib/require-lib.php';
34
35require_once __DIR__ . '/plugin-hotfixes.php';
36
37require_once __DIR__ . '/footer-credit/footer-credit.php';
38require_once __DIR__ . '/storefront/storefront.php';
39require_once __DIR__ . '/custom-colors/colors.php';
40require_once __DIR__ . '/storage/storage.php';
41require_once __DIR__ . '/imports/class-backup-import-manager.php';
42
43// Interoperability with the core WordPress data privacy functionality (See also "GDPR")
44require_once __DIR__ . '/privacy/class-wp-privacy-participating-plugins.php';
45
46// Functionality to make sites private and only accessible to members with appropriate capabilities
47require_once __DIR__ . '/private-site/private-site.php';
48
49// Updates customizer Save/Publish labels to avoid confusion on launching vs saving changes on a site.
50require_once __DIR__ . '/customizer-fixes/customizer-fixes.php';
51
52require_once __DIR__ . '/class-wpcomsh-log.php';
53require_once __DIR__ . '/safeguard/plugins.php';
54require_once __DIR__ . '/jetpack-token-error-header/class-atomic-record-jetpack-token-errors.php';
55
56/**
57 * WP.com Widgets (in alphabetical order)
58 */
59require_once __DIR__ . '/widgets/class-gravatar-widget.php';
60require_once __DIR__ . '/widgets/class-jetpack-posts-i-like-widget.php';
61require_once __DIR__ . '/widgets/class-music-player-widget.php';
62require_once __DIR__ . '/widgets/class-widget-authors-grid.php';
63require_once __DIR__ . '/widgets/class-wpcom-freshly-pressed-widget.php';
64require_once __DIR__ . '/widgets/class-wpcom-widget-recent-comments.php';
65require_once __DIR__ . '/widgets/class-wpcom-widget-reservations.php';
66
67// WP.com Category Cloud widget
68require_once __DIR__ . '/widgets/class-wpcom-category-cloud-widget.php';
69// Override core tag cloud widget to add a settable `limit` parameter
70require_once __DIR__ . '/widgets/class-wpcom-tag-cloud-widget.php';
71
72require_once __DIR__ . '/widgets/tlkio/class-tlkio-widget.php';
73require_once __DIR__ . '/widgets/class-widget-top-clicks.php';
74require_once __DIR__ . '/widgets/class-pd-top-rated.php';
75require_once __DIR__ . '/widgets/class-jetpack-widget-twitter.php';
76
77/*
78 * Autoloader check: This ensures the plugin doesn't fatal if activated before
79 * `composer install` has been run. This is a common oversight during development
80 * setup. The admin notice helps developers quickly identify the issue.
81 */
82$jetpack_autoloader = __DIR__ . '/vendor/autoload_packages.php';
83if ( is_readable( $jetpack_autoloader ) ) {
84    require_once $jetpack_autoloader;
85} else {
86    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
87        error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
88            __( 'Error loading autoloader file for WordPress.com Site Helper plugin', 'wpcomsh' )
89        );
90    }
91
92    add_action(
93        'admin_notices',
94        function () {
95            if ( get_current_screen()->id !== 'plugins' ) {
96                return;
97            }
98
99            $message = sprintf(
100                wp_kses(
101                    /* translators: Placeholder is a link to a support document. */
102                    __( 'Your installation of WordPress.com Site Helper is incomplete. If you installed WordPress.com Site Helper from GitHub, please refer to <a href="%1$s" target="_blank" rel="noopener noreferrer">this document</a> to set up your development environment. WordPress.com Site Helper must have Composer dependencies installed and built via the build command.', 'wpcomsh' ),
103                    array(
104                        'a' => array(
105                            'href'   => array(),
106                            'target' => array(),
107                            'rel'    => array(),
108                        ),
109                    )
110                ),
111                'https://github.com/Automattic/jetpack/blob/trunk/docs/development-environment.md#building-your-project'
112            );
113            wp_admin_notice(
114                $message,
115                array(
116                    'type'        => 'error',
117                    'dismissible' => true,
118                )
119            );
120        }
121    );
122
123    return;
124}
125require_once __DIR__ . '/vendor/automattic/at-pressable-podcasting/podcasting.php';
126require_once __DIR__ . '/vendor/automattic/custom-fonts/custom-fonts.php';
127require_once __DIR__ . '/vendor/automattic/custom-fonts-typekit/custom-fonts-typekit.php';
128require_once __DIR__ . '/vendor/automattic/text-media-widget-styles/text-media-widget-styles.php';
129
130// REST API
131require_once __DIR__ . '/endpoints/rest-api.php';
132
133// Load feature plugins.
134require_once __DIR__ . '/feature-plugins/activitypub.php';
135require_once __DIR__ . '/feature-plugins/additional-css.php';
136require_once __DIR__ . '/feature-plugins/autosave-revision.php';
137require_once __DIR__ . '/feature-plugins/blaze.php';
138require_once __DIR__ . '/feature-plugins/coblocks-mods.php';
139require_once __DIR__ . '/feature-plugins/full-site-editing.php';
140require_once __DIR__ . '/feature-plugins/google-fonts.php';
141require_once __DIR__ . '/feature-plugins/gutenberg-mods.php';
142require_once __DIR__ . '/feature-plugins/headstart-util.php';
143require_once __DIR__ . '/feature-plugins/headstart-woocommerce-terms.php';
144require_once __DIR__ . '/feature-plugins/hooks.php';
145require_once __DIR__ . '/feature-plugins/managed-plugins.php';
146require_once __DIR__ . '/feature-plugins/managed-themes.php';
147require_once __DIR__ . '/feature-plugins/marketplace.php';
148require_once __DIR__ . '/feature-plugins/masterbar.php';
149require_once __DIR__ . '/feature-plugins/migrate-guru-canary.php';
150require_once __DIR__ . '/feature-plugins/nav-redesign.php';
151require_once __DIR__ . '/feature-plugins/post-list.php';
152require_once __DIR__ . '/feature-plugins/class-wpcomsh-recovery-mode-sync.php';
153require_once __DIR__ . '/feature-plugins/sensei-pro-mods.php';
154require_once __DIR__ . '/feature-plugins/smtp-email-priority.php';
155require_once __DIR__ . '/feature-plugins/staging-sites.php';
156require_once __DIR__ . '/feature-plugins/stats.php';
157require_once __DIR__ . '/feature-plugins/woocommerce.php';
158require_once __DIR__ . '/feature-plugins/wordpress-mods.php';
159require_once __DIR__ . '/feature-plugins/wpcom-reader-link.php';
160require_once __DIR__ . '/feature-plugins/reprint-exporter-api.php';
161require_once __DIR__ . '/feature-plugins/featured-image-in-email.php';
162
163/**
164 * Conditionally load the jetpack-mu-wpcom package.
165 *
166 * JETPACK_MU_WPCOM_LOAD_VIA_BETA_PLUGIN=true will load the package via the Jetpack Beta Tester plugin, not wpcomsh.
167 */
168if ( ! defined( 'JETPACK_MU_WPCOM_LOAD_VIA_BETA_PLUGIN' ) || ! JETPACK_MU_WPCOM_LOAD_VIA_BETA_PLUGIN ) {
169    if ( class_exists( 'Automattic\Jetpack\Jetpack_Mu_Wpcom' ) ) {
170        Automattic\Jetpack\Jetpack_Mu_Wpcom::init();
171    }
172}
173
174if ( ! class_exists( 'Jetpack_Data' ) ) {
175    require_once __DIR__ . '/feature-plugins/class-jetpack-data.php';
176}
177
178// Front end notices.
179require_once __DIR__ . '/frontend-notices/wpcomsh-frontend-notices.php';
180
181// wp-admin Notices
182require_once __DIR__ . '/notices/plan-notices.php';
183require_once __DIR__ . '/notices/storage-notices.php';
184require_once __DIR__ . '/notices/php-version-notices.php';
185require_once __DIR__ . '/notices/media-library-private-site-cdn-notice.php';
186require_once __DIR__ . '/notices/anyone-can-register-notice.php';
187require_once __DIR__ . '/notices/feature-moved-to-jetpack-notices.php';
188
189// Performance Profiler
190require_once __DIR__ . '/performance-profiler/performance-profiler.php';
191
192if ( defined( 'WP_CLI' ) && WP_CLI ) {
193    require_once __DIR__ . '/class-wpcomsh-cli-commands.php';
194    require_once __DIR__ . '/woa.php';
195}
196
197require_once __DIR__ . '/wpcom-migration-helpers/site-migration-helpers.php';
198
199// We include WPCom Themes results and installation on non-WP_CLI context.
200if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
201    require_once __DIR__ . '/wpcom-themes/themes.php';
202}
203
204require_once __DIR__ . '/class-jetpack-plugin-compatibility.php';
205Jetpack_Plugin_Compatibility::get_instance();
206
207require_once __DIR__ . '/support-session.php';
208
209// Adds fallback behavior for non-Gutenframed sites to be able to use the 'Share Post' functionality from WPCOM Reader.
210require_once __DIR__ . '/share-post/share-post.php';
211
212// Jetpack Connection Handlers (external storage and protected owner).
213require_once __DIR__ . '/connection/connection-handlers.php';
214
215// Require a Jetpack Connection Owner.
216require_once __DIR__ . '/jetpack-require-connection-owner/class-wpcomsh-require-connection-owner.php';
217
218// Enable MailPoet subscriber stats reports
219require_once __DIR__ . '/mailpoet/class-wpcomsh-mailpoet-subscribers-stats-report.php';
220
221// Force Jetpack to update plugins one-at-a-time to avoid a site-breaking core concurrent update bug
222// https://core.trac.wordpress.org/ticket/53705
223if (
224    ! defined( 'JETPACK_PLUGIN_AUTOUPDATE' ) &&
225    0 === strncmp( $_SERVER['REQUEST_URI'], '/xmlrpc.php?', strlen( '/xmlrpc.php?' ) ) ) { //phpcs:ignore WordPress.Security.ValidatedSanitizedInput
226    define( 'JETPACK_PLUGIN_AUTOUPDATE', true );
227}
228
229/**
230 * Filter attachment URLs if the 'wpcom_attachment_subdomain' option is present.
231 * Local image files will be unaffected, as they will pass a file_exists check.
232 * Files stored remotely will be filtered to have the correct URL.
233 *
234 * Once the files have been transferred, the 'wpcom_attachment_subdomain' will
235 * be removed, preventing further stats.
236 *
237 * @param string $url The attachment URL.
238 * @param int    $post_id The post id.
239 * @return string The filtered attachment URL.
240 */
241function wpcomsh_get_attachment_url( $url, $post_id ) {
242    $attachment_subdomain = get_option( 'wpcom_attachment_subdomain' );
243    if ( $attachment_subdomain ) {
244        $file = get_post_meta( $post_id, '_wp_attached_file', true );
245
246        if ( $file ) {
247            $local_file = WP_CONTENT_DIR . '/uploads/' . $file;
248            if ( ! file_exists( $local_file ) ) {
249                return esc_url( 'https://' . $attachment_subdomain . '/' . $file );
250            }
251        }
252    }
253    return $url;
254}
255add_filter( 'wp_get_attachment_url', 'wpcomsh_get_attachment_url', 11, 2 );
256
257/**
258 * When WordPress.com passes along an expiration for auth cookies and it is smaller
259 * than the value set by Jetpack by default (YEAR_IN_SECONDS), use the smaller value.
260 *
261 * @param int $seconds The cookie expiration in seconds.
262 * @return int The filtered cookie expiration in seconds
263 */
264function wpcomsh_jetpack_sso_auth_cookie_expiration( $seconds ) {
265    if ( isset( $_GET['expires'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
266        $expires = absint( $_GET['expires'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
267
268        if ( ! empty( $expires ) && $expires < $seconds ) {
269            $seconds = $expires;
270        }
271    }
272    return intval( $seconds );
273}
274add_filter( 'jetpack_sso_auth_cookie_expiration', 'wpcomsh_jetpack_sso_auth_cookie_expiration' );
275
276/**
277 * Determine if users should be enforced to log in with their WP.com account.
278 *
279 * Sites without local users:
280 * - WP.com login, always.
281 *
282 * Sites with local users:
283 * - If user comes from Calypso: WP.com login
284 * - Otherwise: Jetpack SSO login, so they can decide whether to use a WP.com account or a local account.
285 */
286function wpcomsh_bypass_jetpack_sso_login() {
287    $calypso_domains = array(
288        'https://wordpress.com/',
289        'https://horizon.wordpress.com/',
290        'https://wpcalypso.wordpress.com/',
291        'http://calypso.localhost:3000/',
292        'http://127.0.0.1:41050/', // Desktop App.
293    );
294    if ( in_array( wp_get_referer(), $calypso_domains, true ) ) {
295        return true;
296    }
297
298    if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) ) {
299        $connection_manager = new \Automattic\Jetpack\Connection\Manager( 'jetpack' );
300
301        // Fetching an extra field to overcome the caching bug: https://core.trac.wordpress.org/ticket/62003
302        $users = get_users( array( 'fields' => array( 'ID', 'user_login' ) ) );
303        foreach ( $users as $user ) {
304            if ( ! $connection_manager->is_user_connected( $user->ID ) ) {
305                return false;
306            }
307        }
308    }
309
310    return true;
311}
312add_filter( 'jetpack_sso_bypass_login_forward_wpcom', 'wpcomsh_bypass_jetpack_sso_login' );
313
314/**
315 * Add 'loggedout' to the list of actions that allow the wpcom login form to be used.
316 *
317 * This means that the login screen the user sees immediately after logging out is consistent
318 * with the login screen the user sees when they are not logged in: the wpcom login form.
319 *
320 * @param array $allowed_actions The allowed actions.
321 * @return array The modified allowed actions.
322 */
323function wpcomsh_modify_jetpack_sso_allowed_actions( $allowed_actions ) {
324    $allowed_actions[] = 'loggedout';
325    return $allowed_actions;
326}
327add_filter( 'jetpack_sso_allowed_actions', 'wpcomsh_modify_jetpack_sso_allowed_actions' );
328
329/**
330 * Overwrite the default value of SSO "Match by Email" setting.
331 * p9o2xV-2zY-p2
332 */
333add_filter( 'default_option_jetpack_sso_match_by_email', '__return_true' );
334
335/**
336 * Admin enqueue style
337 */
338function wpcomsh_admin_enqueue_style() {
339    wp_enqueue_style(
340        'wpcomsh-admin-style',
341        plugins_url( 'assets/admin-style.css', __FILE__ ),
342        array(),
343        WPCOMSH_VERSION
344    );
345}
346add_action( 'admin_enqueue_scripts', 'wpcomsh_admin_enqueue_style', 999 );
347
348/**
349 * Allow custom wp options
350 *
351 * @param array $options The options.
352 *
353 * @return array
354 */
355function wpcomsh_allow_custom_wp_options( $options ) {
356    // For storing AT options.
357    $options[] = 'at_options';
358    $options[] = 'at_options_logging_on';
359    $options[] = 'at_wpcom_premium_theme';
360    $options[] = 'jetpack_fonts';
361    $options[] = 'site_logo';
362    $options[] = 'footercredit';
363    $options[] = 'wpcomsh_at_managed_plugins';
364
365    return $options;
366}
367add_filter( 'jetpack_options_whitelist', 'wpcomsh_allow_custom_wp_options' );
368
369add_filter( 'jetpack_site_automated_transfer', '__return_true' );
370
371/**
372 * Check site has pending automated transfer
373 *
374 * @return bool
375 */
376function check_site_has_pending_automated_transfer() {
377    return get_option( 'has_pending_automated_transfer' );
378}
379
380add_filter( 'jetpack_site_pending_automated_transfer', 'check_site_has_pending_automated_transfer' );
381
382/**
383 * We have some instances where `track_number` of an audio attachment is `??0` and shows up as type string.
384 * However the problem is, that if post has nested property attachments with this track_number, `json_serialize` fails silently.
385 * Of course, this should be fixed during audio upload, but we need this fix until we can clean this up properly.
386 * More detail here: https://github.com/Automattic/automated-transfer/issues/235
387 *
388 * @param array $exif_data The file exif data.
389 *
390 * @return array
391 */
392function wpcomsh_jetpack_api_fix_unserializable_track_number( $exif_data ) {
393    if ( isset( $exif_data['track_number'] ) ) {
394        $exif_data['track_number'] = intval( $exif_data['track_number'] );
395    }
396    return $exif_data;
397}
398add_filter( 'wp_get_attachment_metadata', 'wpcomsh_jetpack_api_fix_unserializable_track_number' );
399
400// Jetpack for Atomic sites are always production version.
401add_filter( 'jetpack_development_version', '__return_false' );
402
403/**
404 * Make User Agent consistent with the rest of WordPress.com.
405 *
406 * @param mixed $agent The agent.
407 */
408function wpcomsh_filter_outgoing_user_agent( $agent ) {
409    global $wp_version;
410
411    return str_replace( "WordPress/$wp_version", 'WordPress.com', $agent );
412}
413add_filter( 'http_headers_useragent', 'wpcomsh_filter_outgoing_user_agent', 999 );
414
415/**
416 * Allow redirects to WordPress.com from Customizer.
417 *
418 * @param array $hosts The hosts.
419 */
420function wpcomsh_allowed_redirect_hosts( $hosts ) {
421    if ( is_array( $hosts ) ) {
422        $hosts[] = 'wordpress.com';
423        $hosts[] = 'calypso.localhost';
424        $hosts   = array_unique( $hosts );
425    }
426    return $hosts;
427}
428add_filter( 'allowed_redirect_hosts', 'wpcomsh_allowed_redirect_hosts', 11 );
429
430/**
431 * WP.com make clickable
432 *
433 * Converts all plain-text HTTP URLs in post_content to links on display.
434 * Uses WP_HTML_Tag_Processor for proper HTML tokenization that won't be confused by
435 * content inside tags (e.g., JavaScript comparison operators in script tags).
436 *
437 * @param string $content The content.
438 * @return string Modified content with linkified URLs.
439 * @uses make_clickable()
440 * @since 20121125
441 */
442function wpcomsh_make_content_clickable( $content ) {
443    // Fast path: no URL-shaped substring, no work to do. Avoids loading the
444    // linkifier and walking the tokenizer for the common case.
445    if ( false === stripos( $content, 'http' ) && false === stripos( $content, 'www.' ) ) {
446        return $content;
447    }
448
449    if ( ! method_exists( 'WP_HTML_Tag_Processor', 'next_token' ) ) {
450        if ( function_exists( 'bump_stats_extras' ) ) {
451            bump_stats_extras( 'wpcomsh-make-content-clickable', 'skipped-no-html-api' );
452        }
453        return $content;
454    }
455
456    require_once __DIR__ . '/class-wpcomsh-html-linkifier.php';
457
458    return Wpcomsh_HTML_Linkifier::modify_raw_text_nodes(
459        $content,
460        static function ( $raw_text ) {
461            return 1 === preg_match( '~https?://|www\.~', $raw_text )
462                ? make_clickable( $raw_text )
463                : $raw_text;
464        }
465    );
466}
467add_filter( 'the_content', 'wpcomsh_make_content_clickable', 120 );
468add_filter( 'the_excerpt', 'wpcomsh_make_content_clickable', 120 );
469
470/**
471 * Hide scan threats from transients
472 *
473 * @param mixed $response The response.
474 *
475 * @return mixed
476 */
477function wpcomsh_hide_scan_threats_from_transients( $response ) {
478    if ( ! empty( $response->threats ) ) {
479        $response->threats = array();
480    }
481    return $response;
482}
483add_filter( 'transient_jetpack_scan_state', 'wpcomsh_hide_scan_threats_from_transients' );
484
485/**
486 * Unhook Jetpack Scan Admin Notice
487 *
488 * @return void
489 */
490function wpcomsh_remove_threats_from_toolbar() {
491    global $wp_admin_bar;
492    $wp_admin_bar->remove_node( 'jetpack-scan-notice' );
493}
494add_action( 'wp_before_admin_bar_render', 'wpcomsh_remove_threats_from_toolbar', 999999 );
495
496/**
497 * Hide scan threats from api
498 *
499 * @param mixed $response The reponse.
500 *
501 * @return mixed
502 */
503function wpcom_hide_scan_threats_from_api( $response ) {
504    if (
505        ! ( $response instanceof WP_REST_Response )
506        || $response->get_matched_route() !== '/jetpack/v4/scan'
507    ) {
508        return $response;
509    }
510    $response_data = $response->get_data();
511    if ( empty( $response_data['data'] ) || ! is_string( $response_data['data'] ) ) {
512        return $response;
513    }
514
515    $json_body = json_decode( $response_data['data'], true );
516    if ( null === $json_body || empty( $json_body['threats'] ) ) {
517        return $response;
518    }
519
520    $json_body['threats']  = array();
521    $response_data['data'] = wp_json_encode( $json_body, JSON_UNESCAPED_SLASHES );
522    $response->set_data( $response_data );
523
524    return $response;
525}
526add_filter( 'rest_post_dispatch', 'wpcom_hide_scan_threats_from_api' );
527
528/**
529 * Returns a standardized timezone string.
530 *
531 * `wp_timezone_string()` sometimes returns offsets (e.g. "-07:00"), which are
532 * a non-standard representation of a UTC offset that only works in PHP.
533 * This function returns a standardized timezone string instead, of the form
534 * "Etc/GMT+7" for integer hour offsets, or a matching "<Area>/<City>" form for
535 * fractional hour offsets (used e.g. in India).
536 */
537function wpcomsh_stats_timezone_string() {
538    $wp_tz = wp_timezone_string();
539
540    // Did we get back an offset?
541    if ( preg_match( '/^([+-])?(\d{1,2}):(\d{2})$/', $wp_tz, $matches ) ) {
542        $sign    = $matches[1] === '-' ? -1 : 1;
543        $hours   = intval( $matches[2], 10 );
544        $minutes = intval( $matches[3], 10 );
545
546        // For fractional hour offsets, use `timezone_name_from_abbr` to get a
547        // matching "<Area>/<City>" timezone.
548        if ( $minutes > 0 ) {
549            $offset  = $sign * ( $hours * 3600 + $minutes * 60 );
550            $city_tz = timezone_name_from_abbr( '', $offset, 0 );
551
552            if ( ! empty( $city_tz ) ) {
553                return $city_tz;
554            }
555        }
556
557        // For integer hour offsets, use "Etc/GMT(+|-)<offset>".
558        // The sign is flipped, to match how the `Etc` area is specced.
559        //
560        // This codepath is also followed if no city exists to match a
561        // fractional offset, by simply discarding the fractional part.
562        // This isn't ideal, but there's no standard way of describing
563        // these offsets, and is likely to be an extreme edge case.
564        return 'Etc/GMT' . ( $sign === -1 ? '+' : '-' ) . $hours;
565    }
566
567    // For anything that's not an offset, return the string we got from WP.
568    return $wp_tz;
569}
570
571/**
572 * Collect RUM performance data
573 * p9o2xV-XY-p2
574 */
575function wpcomsh_footer_rum_js() {
576    $service      = 'atomic';
577    $allow_iframe = '';
578    if ( 'admin_footer' === current_action() ) {
579        $service = 'atomic-wpadmin';
580
581        $block_editor = \Automattic\Jetpack\Jetpack_Mu_Wpcom\WPCOM_Block_Editor\Jetpack_WPCOM_Block_Editor::init();
582        if ( $block_editor->is_iframed_block_editor() ) {
583            $service      = 'atomic-gutenframe';
584            $allow_iframe = 'data-allow-iframe="true"';
585        }
586    }
587
588    $rum_kv = array();
589    $rum_kv = apply_filters( 'wpcomsh_rum_kv', $rum_kv, $service );
590    if ( ! is_array( $rum_kv ) ) {
591        $rum_kv = array();
592    }
593
594    $rum_kv = wpcomsh_get_woo_rum_data( $rum_kv );
595    // Add user login and theme info.
596    $rum_kv['logged_in']        = is_user_logged_in() ? '1' : '0';
597    $rum_kv['wptheme']          = get_stylesheet();
598    $rum_kv['wptheme_is_block'] = wp_is_block_theme() ? '1' : '0';
599
600    if ( count( $rum_kv ) > 0 ) {
601        $rum_kv = wp_json_encode( $rum_kv, JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_HEX_AMP );
602        if ( is_string( $rum_kv ) ) {
603            $rum_kv = 'data-customproperties="' . esc_attr( $rum_kv ) . '"';
604        } else {
605            $rum_kv = '';
606        }
607    } else {
608        $rum_kv = '';
609    }
610
611    $data_site_tz = 'data-site-tz="' . esc_attr( wpcomsh_stats_timezone_string() ) . '"';
612
613    printf(
614        '<meta id="bilmur" property="bilmur:data" content="" %1$s data-provider="wordpress.com" data-service="%2$s" %3$s %4$s >' . "\n",
615        $rum_kv, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
616        esc_attr( $service ),
617        wp_kses_post( $allow_iframe ),
618        $data_site_tz // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
619    );
620    printf(
621        '<script defer src="%s"></script>' . "\n", //phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
622        esc_url( 'https://s0.wp.com/wp-content/js/bilmur.min.js?m=' . gmdate( 'YW' ) )
623    );
624}
625
626/**
627 * Adds WooCommerce-related data to the Real User Monitoring (RUM) array.
628 *
629 * This function checks if WooCommerce is active on the site and adds
630 * this information to the provided RUM data array. It's designed to be
631 * used as part of the RUM data collection process for Atomic sites.
632 *
633 * @param array $rum_kv An array of existing RUM key-value pairs.
634 *                      If not provided, an empty array will be used.
635 *
636 * @return array The input array with added WooCommerce data.
637 *               The 'woo_active' key will be added with a boolean value
638 *               indicating whether WooCommerce is active.
639 */
640function wpcomsh_get_woo_rum_data( $rum_kv = array() ) {
641    $woo_active           = class_exists( 'WooCommerce' ) ? '1' : '0';
642    $rum_kv['woo_active'] = $woo_active;
643    return $rum_kv;
644}
645
646add_action( 'wp_footer', 'wpcomsh_footer_rum_js' );
647add_action( 'admin_footer', 'wpcomsh_footer_rum_js' );
648
649/**
650 * Adds Atomic site ID to WooCommerce tracker data.
651 *
652 * @param array $data The WooCommerce tracker data.
653 *
654 * @return array The WooCommerce tracker data with Atomic site ID added.
655 */
656function wpcomsh_woocommerce_tracker_data( $data ) {
657    $data['atomic_site_id'] = wpcomsh_get_atomic_site_id();
658    return $data;
659}
660
661add_filter( 'woocommerce_tracker_data', 'wpcomsh_woocommerce_tracker_data' );
662
663add_filter( 'amp_dev_tools_user_default_enabled', '__return_false' );
664
665/**
666 * Tracks helper. Filters Jetpack TOS option if class exists.
667 *
668 * @param mixed $event The event.
669 * @param mixed $event_properties The event property.
670 *
671 * @return void
672 */
673function wpcomsh_record_tracks_event( $event, $event_properties ) {
674    if ( class_exists( '\Automattic\Jetpack\Tracking' ) ) {
675        // User has to agree to ToS for tracking. Thing is, on initial Simple -> Atomic we never set the ToS option.
676        // And since they agreed to WP.com ToS, we can track but in a roundabout way. :).
677        add_filter( 'jetpack_options', 'wpcomsh_jetpack_filter_tos_for_tracking', 10, 2 );
678
679        $jetpack_tracks = new \Automattic\Jetpack\Tracking( 'atomic' );
680        $jetpack_tracks->tracks_record_event(
681            wp_get_current_user(),
682            $event,
683            $event_properties
684        );
685
686        remove_filter( 'jetpack_options', 'wpcomsh_jetpack_filter_tos_for_tracking', 10 );
687    }
688}
689
690/**
691 * Helper for filtering tos_agreed for tracking purposes.
692 * Explicit function so it can be removed afterwards.
693 *
694 * @param mixed $value The value.
695 * @param mixed $name Name.
696 *
697 * @return mixed
698 */
699function wpcomsh_jetpack_filter_tos_for_tracking( $value, $name ) {
700    if ( 'tos_agreed' === $name ) {
701        return true;
702    }
703
704    return $value;
705}
706
707/**
708 * Avoid proxied v2 banner
709 *
710 * @return void
711 */
712function wpcomsh_avoid_proxied_v2_banner() {
713    $priority = has_action( 'wp_footer', 'atomic_proxy_bar' );
714    if ( false !== $priority ) {
715        remove_action( 'wp_footer', 'atomic_proxy_bar', $priority );
716    }
717
718    $priority = has_action( 'admin_footer', 'atomic_proxy_bar' );
719    if ( false !== $priority ) {
720        remove_action( 'admin_footer', 'atomic_proxy_bar', $priority );
721    }
722}
723
724// We don't want to show a "PROXIED V2" banner for legacy widget previews
725// which are normally embedded within another page.
726if (
727    defined( 'AT_PROXIED_REQUEST' ) && AT_PROXIED_REQUEST &&
728    isset( $_GET['legacy-widget-preview'] ) && //phpcs:ignore WordPress.Security.NonceVerification
729    0 === strncmp( $_SERVER['REQUEST_URI'], '/wp-admin/widgets.php?', strlen( '/wp-admin/widgets.php?' ) ) ) { //phpcs:ignore WordPress.Security.ValidatedSanitizedInput
730    add_action( 'plugins_loaded', 'wpcomsh_avoid_proxied_v2_banner' );
731}