Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.84% covered (warning)
86.84%
99 / 114
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Status
86.84% covered (warning)
86.84%
99 / 114
33.33% covered (danger)
33.33%
3 / 9
69.48
0.00% covered (danger)
0.00%
0 / 1
 is_offline_mode
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
13
 is_multi_network
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 is_single_user_site
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 is_local_site
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
12
 is_staging_site
n/a
0 / 0
n/a
0 / 0
12
 in_safe_mode
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 is_development_site
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 is_onboarding
n/a
0 / 0
n/a
0 / 0
1
 is_private_site
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 is_coming_soon
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 get_site_suffix
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
1<?php
2/**
3 * A status class for Jetpack.
4 *
5 * @package automattic/jetpack-status
6 */
7
8namespace Automattic\Jetpack;
9
10use Automattic\Jetpack\Status\Cache;
11use Automattic\Jetpack\Status\Host;
12use WPCOM_Masterbar;
13
14/**
15 * Class Automattic\Jetpack\Status
16 *
17 * Used to retrieve information about the current status of Jetpack and the site overall.
18 */
19class Status {
20    /**
21     * Is Jetpack in offline mode?
22     *
23     * This was formerly called "Development Mode", but sites "in development" aren't always offline/localhost.
24     *
25     * @since 1.3.0
26     *
27     * @return bool Whether Jetpack's offline mode is active.
28     */
29    public function is_offline_mode() {
30        $cached = Cache::get( 'is_offline_mode' );
31        if ( null !== $cached ) {
32            return $cached;
33        }
34
35        $offline_mode = false;
36
37        if ( defined( '\\JETPACK_DEV_DEBUG' ) ) {
38            $offline_mode = constant( '\\JETPACK_DEV_DEBUG' );
39        } elseif ( defined( '\\WP_LOCAL_DEV' ) ) {
40            $offline_mode = constant( '\\WP_LOCAL_DEV' );
41        } elseif ( $this->is_local_site() ) {
42            $offline_mode = true;
43        }
44
45        /**
46         * Filters Jetpack's offline mode.
47         *
48         * @see https://jetpack.com/support/offline-mode/
49         *
50         * @since 1.3.0
51         *
52         * @param bool $offline_mode Is Jetpack's offline mode active.
53         */
54        $offline_mode = (bool) apply_filters( 'jetpack_offline_mode', $offline_mode );
55
56        if ( ! $offline_mode ) {
57            // Sentinel default so an absent option isn't confused with a stored null/false.
58            $sentinel = new \stdClass();
59            $option   = get_option( 'jetpack_offline_mode', $sentinel );
60
61            if ( $sentinel === $option ) {
62                // Seed an autoloaded default to stop per-request reads, but only where it helps
63                // (no persistent object cache) and writes are safe (keep front-end reads read-only).
64                if ( ! wp_using_ext_object_cache() && ( is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) ) {
65                    add_option( 'jetpack_offline_mode', false, '', true );
66                }
67            } else {
68                // A default_option filter could return a non-scalar; only a scalar is a real value.
69                $offline_mode = is_scalar( $option ) ? (bool) $option : false;
70            }
71        }
72
73        Cache::set( 'is_offline_mode', $offline_mode );
74        return $offline_mode;
75    }
76
77    /**
78     * Whether this is a system with a multiple networks.
79     * Implemented since there is no core is_multi_network function.
80     * Right now there is no way to tell which network is the dominant network on the system.
81     *
82     * @return boolean
83     */
84    public function is_multi_network() {
85        global $wpdb;
86
87        $cached = Cache::get( 'is_multi_network' );
88        if ( null !== $cached ) {
89            return $cached;
90        }
91
92        // If we don't have a multi site setup no need to do any more.
93        if ( ! is_multisite() ) {
94            Cache::set( 'is_multi_network', false );
95            return false;
96        }
97
98        $num_sites = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->site}" );
99        if ( $num_sites > 1 ) {
100            Cache::set( 'is_multi_network', true );
101            return true;
102        }
103
104        Cache::set( 'is_multi_network', false );
105        return false;
106    }
107
108    /**
109     * Whether the current site is single user site.
110     *
111     * @return bool
112     */
113    public function is_single_user_site() {
114        global $wpdb;
115
116        $ret = Cache::get( 'is_single_user_site' );
117        if ( null === $ret ) {
118            $some_users = get_transient( 'jetpack_is_single_user' );
119            if ( false === $some_users ) {
120                $some_users = $wpdb->get_var( "SELECT COUNT(*) FROM (SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}capabilities' LIMIT 2) AS someusers" );
121                set_transient( 'jetpack_is_single_user', (int) $some_users, 12 * HOUR_IN_SECONDS );
122            }
123            $ret = 1 === (int) $some_users;
124            Cache::set( 'is_single_user_site', $ret );
125        }
126        return $ret;
127    }
128
129    /**
130     * If the site is a local site.
131     *
132     * @since 1.3.0
133     *
134     * @return bool
135     */
136    public function is_local_site() {
137        $cached = Cache::get( 'is_local_site' );
138        if ( null !== $cached ) {
139            return $cached;
140        }
141
142        $site_url = trim( (string) site_url() );
143
144        /*
145         * Every check below cares about the host, not the rest of the URL. Matching the host
146         * alone means a port, a subdirectory install, or a trailing slash can't defeat them,
147         * and a known local domain sitting in the path can't trigger them by accident.
148         */
149        $host = wp_parse_url( $site_url, PHP_URL_HOST );
150        if ( ( ! is_string( $host ) || '' === $host ) && ! preg_match( '#^[a-z][a-z0-9+.\-]*:#i', $site_url ) ) {
151            /*
152             * No scheme, so site_url() isn't absolute. Read the value as a bare host.
153             * A value that does carry a scheme but still won't parse is malformed, and
154             * guessing at it would read "https:/example.com" as the host "https".
155             */
156            $host = wp_parse_url( '//' . ltrim( $site_url, '/' ), PHP_URL_HOST );
157        }
158        if ( ! is_string( $host ) ) {
159            /*
160             * Nothing host-shaped to test. Treat the site as remote rather than matching the
161             * raw URL: a false "local" silently drops a live site into offline mode.
162             */
163            $host = '';
164        }
165
166        /*
167         * Check for localhost and sites using an IP only first. No dot means it can't be a
168         * public domain. Dotless IPv6 literals land here too, routable ones included:
169         * WordPress.com won't register an IPv6 site URL, so offline mode is where they belong.
170         * An IPv4-mapped literal like [::ffff:127.0.0.1] keeps its dots and reads as remote.
171         */
172        $is_local = '' !== $host && false === strpos( $host, '.' );
173
174        // Use Core's environment check, if available.
175        if ( 'local' === wp_get_environment_type() ) {
176            $is_local = true;
177        }
178
179        // Then check for usual domains used by local dev tools.
180        $known_local = array(
181            '#\.local$#i',
182            '#(^|\.)localhost$#i', // localhost and any subdomain of it.
183            '#\.test$#i',
184            '#\.docksal$#i',       // Docksal.
185            '#\.docksal\.site$#i', // Docksal.
186            '#\.dev\.cc$#i',       // ServerPress.
187            '#\.lndo\.site$#i',    // Lando.
188            '#\.ddev\.site$#i',    // DDEV.
189            // The whole 127.0.0.0/8 range is loopback; each octet is capped at 255.
190            '#^127\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){2}$#',
191            '#^0\.0\.0\.0$#', // All-interfaces bind, common under Docker and `wp server`.
192            '#^playground\.wordpress\.net$#i', // WordPress Playground, which runs entirely in the browser.
193        );
194
195        if ( ! $is_local && '' !== $host ) {
196            foreach ( $known_local as $pattern ) {
197                if ( preg_match( $pattern, $host ) ) {
198                    $is_local = true;
199                    break;
200                }
201            }
202        }
203
204        /**
205         * Filters is_local_site check.
206         *
207         * @since 1.3.0
208         *
209         * @param bool $is_local If the current site is a local site.
210         */
211        $is_local = apply_filters( 'jetpack_is_local_site', $is_local );
212
213        Cache::set( 'is_local_site', $is_local );
214        return $is_local;
215    }
216
217    /**
218     * If is a staging site.
219     *
220     * @deprecated since 3.3.0
221     *
222     * @return bool
223     */
224    public function is_staging_site() {
225        _deprecated_function( __FUNCTION__, '3.3.0', 'in_safe_mode' );
226        $cached = Cache::get( 'is_staging_site' );
227        if ( null !== $cached ) {
228            return $cached;
229        }
230
231        /*
232         * Core's wp_get_environment_type allows for a few specific options.
233         * We should default to bowing out gracefully for anything other than production or local.
234         */
235        $is_staging = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
236
237        $known_staging = array(
238            'urls'      => array(
239                '#\.staging\.wpengine\.com$#i',                    // WP Engine. This is their legacy staging URL structure. Their new platform does not have a common URL. https://github.com/Automattic/jetpack/issues/21504
240                '#\.staging\.kinsta\.com$#i',                      // Kinsta.com.
241                '#\.kinsta\.cloud$#i',                             // Kinsta.com.
242                '#\.stage\.site$#i',                               // DreamPress.
243                '#\.newspackstaging\.com$#i',                      // Newspack.
244                '#^(?!live-)([a-zA-Z0-9-]+)\.pantheonsite\.io$#i', // Pantheon.
245                '#\.flywheelsites\.com$#i',                        // Flywheel.
246                '#\.flywheelstaging\.com$#i',                      // Flywheel.
247                '#\.cloudwaysapps\.com$#i',                        // Cloudways.
248                '#\.azurewebsites\.net$#i',                        // Azure.
249                '#\.wpserveur\.net$#i',                            // WPServeur.
250                '#\-liquidwebsites\.com$#i',                       // Liquidweb.
251            ),
252            'constants' => array(
253                'IS_WPE_SNAPSHOT',      // WP Engine. This is used on their legacy staging environment. Their new platform does not have a constant. https://github.com/Automattic/jetpack/issues/21504
254                'KINSTA_DEV_ENV',       // Kinsta.com.
255                'WPSTAGECOACH_STAGING', // WP Stagecoach.
256                'JETPACK_STAGING_MODE', // Generic.
257                'WP_LOCAL_DEV',         // Generic.
258            ),
259        );
260        /**
261         * Filters the flags of known staging sites.
262         *
263         * @since 1.1.1
264         * @since-jetpack 3.9.0
265         *
266         * @param array $known_staging {
267         *     An array of arrays that each are used to check if the current site is staging.
268         *     @type array $urls      URLs of staging sites in regex to check against site_url.
269         *     @type array $constants PHP constants of known staging/developement environments.
270         *  }
271         */
272        $known_staging = apply_filters( 'jetpack_known_staging', $known_staging );
273
274        if ( isset( $known_staging['urls'] ) ) {
275            $site_url = site_url();
276            foreach ( $known_staging['urls'] as $url ) {
277                if ( preg_match( $url, wp_parse_url( $site_url, PHP_URL_HOST ) ) ) {
278                    $is_staging = true;
279                    break;
280                }
281            }
282        }
283
284        if ( isset( $known_staging['constants'] ) ) {
285            foreach ( $known_staging['constants'] as $constant ) {
286                if ( defined( $constant ) && constant( $constant ) ) {
287                    $is_staging = true;
288                }
289            }
290        }
291
292        // Last, let's check if sync is erroring due to an IDC. If so, set the site to staging mode.
293        if ( ! $is_staging && method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
294            $is_staging = true;
295        }
296
297        /**
298         * Filters is_staging_site check.
299         *
300         * @since 1.1.1
301         * @since-jetpack 3.9.0
302         *
303         * @param bool $is_staging If the current site is a staging site.
304         */
305        $is_staging = apply_filters( 'jetpack_is_staging_site', $is_staging );
306
307        Cache::set( 'is_staging_site', $is_staging );
308        return $is_staging;
309    }
310
311    /**
312     * If the site is in safe mode.
313     *
314     * @since 3.3.0
315     *
316     * @return bool
317     */
318    public function in_safe_mode() {
319        $cached = Cache::get( 'in_safe_mode' );
320        if ( null !== $cached ) {
321            return $cached;
322        }
323        $in_safe_mode = false;
324        if ( method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
325            $in_safe_mode = true;
326        }
327        /**
328         * Filters in_safe_mode check.
329         *
330         * @since 3.3.0
331         *
332         * @param bool $in_safe_mode If the current site is in safe mode.
333         */
334        $in_safe_mode = apply_filters( 'jetpack_is_in_safe_mode', $in_safe_mode );
335
336        Cache::set( 'in_safe_mode', $in_safe_mode );
337        return $in_safe_mode;
338    }
339
340    /**
341     * If the site is a development/staging site.
342     * This is a new version of is_staging_site added to separate safe mode from the legacy staging mode.
343     * This method checks for core WP_ENVIRONMENT_TYPE setting
344     * Using the jetpack_is_development_site filter.
345     *
346     * @since 3.3.0
347     *
348     * @return bool
349     */
350    public static function is_development_site() {
351        $cached = Cache::get( 'is_development_site' );
352        if ( null !== $cached ) {
353            return $cached;
354        }
355        $is_dev_site = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
356        /**
357         * Filters is_development_site check.
358         *
359         * @since 3.3.0
360         *
361         * @param bool $is_dev_site If the current site is a staging or dev site.
362         */
363        $is_dev_site = apply_filters( 'jetpack_is_development_site', $is_dev_site );
364
365        Cache::set( 'is_development_site', $is_dev_site );
366        return $is_dev_site;
367    }
368
369    /**
370     * Whether the site is currently onboarding or not.
371     * A site is considered as being onboarded if it currently has an onboarding token.
372     *
373     * @since-jetpack 5.8
374     *
375     * @deprecated since 4.0.0
376     *
377     * @access public
378     * @static
379     *
380     * @return bool True if the site is currently onboarding, false otherwise
381     */
382    public function is_onboarding() {
383        return \Jetpack_Options::get_option( 'onboarding' ) !== false;
384    }
385
386    /**
387     * Whether the site is currently private or not.
388     * On WordPress.com and WoA, sites can be marked as private
389     *
390     * @since 1.16.0
391     *
392     * @return bool True if the site is private.
393     */
394    public function is_private_site() {
395        $ret = Cache::get( 'is_private_site' );
396        if ( null === $ret ) {
397            $is_private_site = '-1' === get_option( 'blog_public' );
398
399            /**
400             * Filters the is_private_site check.
401             *
402             * @since 1.16.1
403             *
404             * @param bool $is_private_site True if the site is private.
405             */
406            $is_private_site = apply_filters( 'jetpack_is_private_site', $is_private_site );
407
408            Cache::set( 'is_private_site', $is_private_site );
409            return $is_private_site;
410        }
411        return $ret;
412    }
413
414    /**
415     * Whether the site is currently unlaunched or not.
416     * On WordPress.com and WoA, sites can be marked as "coming soon", aka unlaunched
417     *
418     * @since 1.16.0
419     *
420     * @return bool True if the site is not launched.
421     */
422    public function is_coming_soon() {
423        $ret = Cache::get( 'is_coming_soon' );
424        if ( null === $ret ) {
425            $is_coming_soon = ( function_exists( 'site_is_coming_soon' ) && \site_is_coming_soon() )
426                || get_option( 'wpcom_public_coming_soon' );
427
428            /**
429             * Filters the is_coming_soon check.
430             *
431             * @since 1.16.1
432             *
433             * @param bool $is_coming_soon True if the site is coming soon (i.e. unlaunched).
434             */
435            $is_coming_soon = apply_filters( 'jetpack_is_coming_soon', $is_coming_soon );
436
437            Cache::set( 'is_coming_soon', $is_coming_soon );
438            return $is_coming_soon;
439        }
440        return $ret;
441    }
442
443    /**
444     * Returns the site slug suffix to be used as part of Calypso URLs.
445     *
446     * Strips http:// or https:// from a url, replaces forward slash with ::.
447     *
448     * @since 1.6.0
449     *
450     * @param string $url Optional. URL to build the site suffix from. Default: Home URL.
451     *
452     * @return string
453     */
454    public function get_site_suffix( $url = '' ) {
455        // On WordPress.com, site suffixes are a bit different.
456        if ( method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
457            return WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
458        }
459
460        // Grab the 'site_url' option for WoA sites to avoid plugins to interfere with the site
461        // identifier (e.g. i18n plugins may change the main url to '<DOMAIN>/<LOCALE>', but we
462        // want to exclude the locale since it's not part of the site suffix).
463        if ( ( new Host() )->is_woa_site() ) {
464            $url = \site_url();
465        }
466
467        if ( empty( $url ) ) {
468            // WordPress can be installed in subdirectories (e.g. make.wordpress.org/plugins)
469            // where the 'site_url' option points to the root domain (e.g. make.wordpress.org)
470            // which could collide with another site in the same domain but with WordPress
471            // installed in a different subdirectory (e.g. make.wordpress.org/core). To avoid
472            // such collision, we identify the site with the 'home_url' option.
473            $url = \home_url();
474        }
475
476        $url = preg_replace( '#^.*?://#', '', $url );
477        $url = str_replace( '/', '::', $url );
478
479        return rtrim( $url, ':' );
480    }
481}