Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.73% covered (warning)
86.73%
98 / 113
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Status
86.73% covered (warning)
86.73%
98 / 113
33.33% covered (danger)
33.33%
3 / 9
69.70
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%
32 / 32
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        // Check for localhost and sites using an IP only first.
167        $is_local = '' !== $host && false === strpos( $host, '.' );
168
169        // Use Core's environment check, if available.
170        if ( 'local' === wp_get_environment_type() ) {
171            $is_local = true;
172        }
173
174        // Then check for usual domains used by local dev tools.
175        $known_local = array(
176            '#\.local$#i',
177            '#(^|\.)localhost$#i', // localhost and any subdomain of it.
178            '#\.test$#i',
179            '#\.docksal$#i',       // Docksal.
180            '#\.docksal\.site$#i', // Docksal.
181            '#\.dev\.cc$#i',       // ServerPress.
182            '#\.lndo\.site$#i',    // Lando.
183            '#\.ddev\.site$#i',    // DDEV.
184            '#^127\.0\.0\.1$#',
185            '#^playground\.wordpress\.net$#i', // WordPress Playground, which runs entirely in the browser.
186        );
187
188        if ( ! $is_local && '' !== $host ) {
189            foreach ( $known_local as $pattern ) {
190                if ( preg_match( $pattern, $host ) ) {
191                    $is_local = true;
192                    break;
193                }
194            }
195        }
196
197        /**
198         * Filters is_local_site check.
199         *
200         * @since 1.3.0
201         *
202         * @param bool $is_local If the current site is a local site.
203         */
204        $is_local = apply_filters( 'jetpack_is_local_site', $is_local );
205
206        Cache::set( 'is_local_site', $is_local );
207        return $is_local;
208    }
209
210    /**
211     * If is a staging site.
212     *
213     * @deprecated since 3.3.0
214     *
215     * @return bool
216     */
217    public function is_staging_site() {
218        _deprecated_function( __FUNCTION__, '3.3.0', 'in_safe_mode' );
219        $cached = Cache::get( 'is_staging_site' );
220        if ( null !== $cached ) {
221            return $cached;
222        }
223
224        /*
225         * Core's wp_get_environment_type allows for a few specific options.
226         * We should default to bowing out gracefully for anything other than production or local.
227         */
228        $is_staging = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
229
230        $known_staging = array(
231            'urls'      => array(
232                '#\.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
233                '#\.staging\.kinsta\.com$#i',                      // Kinsta.com.
234                '#\.kinsta\.cloud$#i',                             // Kinsta.com.
235                '#\.stage\.site$#i',                               // DreamPress.
236                '#\.newspackstaging\.com$#i',                      // Newspack.
237                '#^(?!live-)([a-zA-Z0-9-]+)\.pantheonsite\.io$#i', // Pantheon.
238                '#\.flywheelsites\.com$#i',                        // Flywheel.
239                '#\.flywheelstaging\.com$#i',                      // Flywheel.
240                '#\.cloudwaysapps\.com$#i',                        // Cloudways.
241                '#\.azurewebsites\.net$#i',                        // Azure.
242                '#\.wpserveur\.net$#i',                            // WPServeur.
243                '#\-liquidwebsites\.com$#i',                       // Liquidweb.
244            ),
245            'constants' => array(
246                '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
247                'KINSTA_DEV_ENV',       // Kinsta.com.
248                'WPSTAGECOACH_STAGING', // WP Stagecoach.
249                'JETPACK_STAGING_MODE', // Generic.
250                'WP_LOCAL_DEV',         // Generic.
251            ),
252        );
253        /**
254         * Filters the flags of known staging sites.
255         *
256         * @since 1.1.1
257         * @since-jetpack 3.9.0
258         *
259         * @param array $known_staging {
260         *     An array of arrays that each are used to check if the current site is staging.
261         *     @type array $urls      URLs of staging sites in regex to check against site_url.
262         *     @type array $constants PHP constants of known staging/developement environments.
263         *  }
264         */
265        $known_staging = apply_filters( 'jetpack_known_staging', $known_staging );
266
267        if ( isset( $known_staging['urls'] ) ) {
268            $site_url = site_url();
269            foreach ( $known_staging['urls'] as $url ) {
270                if ( preg_match( $url, wp_parse_url( $site_url, PHP_URL_HOST ) ) ) {
271                    $is_staging = true;
272                    break;
273                }
274            }
275        }
276
277        if ( isset( $known_staging['constants'] ) ) {
278            foreach ( $known_staging['constants'] as $constant ) {
279                if ( defined( $constant ) && constant( $constant ) ) {
280                    $is_staging = true;
281                }
282            }
283        }
284
285        // Last, let's check if sync is erroring due to an IDC. If so, set the site to staging mode.
286        if ( ! $is_staging && method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
287            $is_staging = true;
288        }
289
290        /**
291         * Filters is_staging_site check.
292         *
293         * @since 1.1.1
294         * @since-jetpack 3.9.0
295         *
296         * @param bool $is_staging If the current site is a staging site.
297         */
298        $is_staging = apply_filters( 'jetpack_is_staging_site', $is_staging );
299
300        Cache::set( 'is_staging_site', $is_staging );
301        return $is_staging;
302    }
303
304    /**
305     * If the site is in safe mode.
306     *
307     * @since 3.3.0
308     *
309     * @return bool
310     */
311    public function in_safe_mode() {
312        $cached = Cache::get( 'in_safe_mode' );
313        if ( null !== $cached ) {
314            return $cached;
315        }
316        $in_safe_mode = false;
317        if ( method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
318            $in_safe_mode = true;
319        }
320        /**
321         * Filters in_safe_mode check.
322         *
323         * @since 3.3.0
324         *
325         * @param bool $in_safe_mode If the current site is in safe mode.
326         */
327        $in_safe_mode = apply_filters( 'jetpack_is_in_safe_mode', $in_safe_mode );
328
329        Cache::set( 'in_safe_mode', $in_safe_mode );
330        return $in_safe_mode;
331    }
332
333    /**
334     * If the site is a development/staging site.
335     * This is a new version of is_staging_site added to separate safe mode from the legacy staging mode.
336     * This method checks for core WP_ENVIRONMENT_TYPE setting
337     * Using the jetpack_is_development_site filter.
338     *
339     * @since 3.3.0
340     *
341     * @return bool
342     */
343    public static function is_development_site() {
344        $cached = Cache::get( 'is_development_site' );
345        if ( null !== $cached ) {
346            return $cached;
347        }
348        $is_dev_site = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
349        /**
350         * Filters is_development_site check.
351         *
352         * @since 3.3.0
353         *
354         * @param bool $is_dev_site If the current site is a staging or dev site.
355         */
356        $is_dev_site = apply_filters( 'jetpack_is_development_site', $is_dev_site );
357
358        Cache::set( 'is_development_site', $is_dev_site );
359        return $is_dev_site;
360    }
361
362    /**
363     * Whether the site is currently onboarding or not.
364     * A site is considered as being onboarded if it currently has an onboarding token.
365     *
366     * @since-jetpack 5.8
367     *
368     * @deprecated since 4.0.0
369     *
370     * @access public
371     * @static
372     *
373     * @return bool True if the site is currently onboarding, false otherwise
374     */
375    public function is_onboarding() {
376        return \Jetpack_Options::get_option( 'onboarding' ) !== false;
377    }
378
379    /**
380     * Whether the site is currently private or not.
381     * On WordPress.com and WoA, sites can be marked as private
382     *
383     * @since 1.16.0
384     *
385     * @return bool True if the site is private.
386     */
387    public function is_private_site() {
388        $ret = Cache::get( 'is_private_site' );
389        if ( null === $ret ) {
390            $is_private_site = '-1' === get_option( 'blog_public' );
391
392            /**
393             * Filters the is_private_site check.
394             *
395             * @since 1.16.1
396             *
397             * @param bool $is_private_site True if the site is private.
398             */
399            $is_private_site = apply_filters( 'jetpack_is_private_site', $is_private_site );
400
401            Cache::set( 'is_private_site', $is_private_site );
402            return $is_private_site;
403        }
404        return $ret;
405    }
406
407    /**
408     * Whether the site is currently unlaunched or not.
409     * On WordPress.com and WoA, sites can be marked as "coming soon", aka unlaunched
410     *
411     * @since 1.16.0
412     *
413     * @return bool True if the site is not launched.
414     */
415    public function is_coming_soon() {
416        $ret = Cache::get( 'is_coming_soon' );
417        if ( null === $ret ) {
418            $is_coming_soon = ( function_exists( 'site_is_coming_soon' ) && \site_is_coming_soon() )
419                || get_option( 'wpcom_public_coming_soon' );
420
421            /**
422             * Filters the is_coming_soon check.
423             *
424             * @since 1.16.1
425             *
426             * @param bool $is_coming_soon True if the site is coming soon (i.e. unlaunched).
427             */
428            $is_coming_soon = apply_filters( 'jetpack_is_coming_soon', $is_coming_soon );
429
430            Cache::set( 'is_coming_soon', $is_coming_soon );
431            return $is_coming_soon;
432        }
433        return $ret;
434    }
435
436    /**
437     * Returns the site slug suffix to be used as part of Calypso URLs.
438     *
439     * Strips http:// or https:// from a url, replaces forward slash with ::.
440     *
441     * @since 1.6.0
442     *
443     * @param string $url Optional. URL to build the site suffix from. Default: Home URL.
444     *
445     * @return string
446     */
447    public function get_site_suffix( $url = '' ) {
448        // On WordPress.com, site suffixes are a bit different.
449        if ( method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
450            return WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
451        }
452
453        // Grab the 'site_url' option for WoA sites to avoid plugins to interfere with the site
454        // identifier (e.g. i18n plugins may change the main url to '<DOMAIN>/<LOCALE>', but we
455        // want to exclude the locale since it's not part of the site suffix).
456        if ( ( new Host() )->is_woa_site() ) {
457            $url = \site_url();
458        }
459
460        if ( empty( $url ) ) {
461            // WordPress can be installed in subdirectories (e.g. make.wordpress.org/plugins)
462            // where the 'site_url' option points to the root domain (e.g. make.wordpress.org)
463            // which could collide with another site in the same domain but with WordPress
464            // installed in a different subdirectory (e.g. make.wordpress.org/core). To avoid
465            // such collision, we identify the site with the 'home_url' option.
466            $url = \home_url();
467        }
468
469        $url = preg_replace( '#^.*?://#', '', $url );
470        $url = str_replace( '/', '::', $url );
471
472        return rtrim( $url, ':' );
473    }
474}