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