<?php
/**
 * A status class for Jetpack.
 *
 * @package automattic/jetpack-status
 */

namespace Automattic\Jetpack;

use Automattic\Jetpack\Status\Cache;
use Automattic\Jetpack\Status\Host;
use WPCOM_Masterbar;

/**
 * Class Automattic\Jetpack\Status
 *
 * Used to retrieve information about the current status of Jetpack and the site overall.
 */
class Status {
	/**
	 * Is Jetpack in offline mode?
	 *
	 * This was formerly called "Development Mode", but sites "in development" aren't always offline/localhost.
	 *
	 * @since 1.3.0
	 *
	 * @return bool Whether Jetpack's offline mode is active.
	 */
	public function is_offline_mode() {
		$cached = Cache::get( 'is_offline_mode' );
		if ( null !== $cached ) {
			return $cached;
		}

		$offline_mode = false;

		if ( defined( '\\JETPACK_DEV_DEBUG' ) ) {
			$offline_mode = constant( '\\JETPACK_DEV_DEBUG' );
		} elseif ( defined( '\\WP_LOCAL_DEV' ) ) {
			$offline_mode = constant( '\\WP_LOCAL_DEV' );
		} elseif ( $this->is_local_site() ) {
			$offline_mode = true;
		}

		/**
		 * Filters Jetpack's offline mode.
		 *
		 * @see https://jetpack.com/support/offline-mode/
		 *
		 * @since 1.3.0
		 *
		 * @param bool $offline_mode Is Jetpack's offline mode active.
		 */
		$offline_mode = (bool) apply_filters( 'jetpack_offline_mode', $offline_mode );

		if ( ! $offline_mode ) {
			// Sentinel default so an absent option isn't confused with a stored null/false.
			$sentinel = new \stdClass();
			$option   = get_option( 'jetpack_offline_mode', $sentinel );

			if ( $sentinel === $option ) {
				// Seed an autoloaded default to stop per-request reads, but only where it helps
				// (no persistent object cache) and writes are safe (keep front-end reads read-only).
				if ( ! wp_using_ext_object_cache() && ( is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) ) {
					add_option( 'jetpack_offline_mode', false, '', true );
				}
			} else {
				// A default_option filter could return a non-scalar; only a scalar is a real value.
				$offline_mode = is_scalar( $option ) ? (bool) $option : false;
			}
		}

		Cache::set( 'is_offline_mode', $offline_mode );
		return $offline_mode;
	}

	/**
	 * Whether this is a system with a multiple networks.
	 * Implemented since there is no core is_multi_network function.
	 * Right now there is no way to tell which network is the dominant network on the system.
	 *
	 * @return boolean
	 */
	public function is_multi_network() {
		global $wpdb;

		$cached = Cache::get( 'is_multi_network' );
		if ( null !== $cached ) {
			return $cached;
		}

		// If we don't have a multi site setup no need to do any more.
		if ( ! is_multisite() ) {
			Cache::set( 'is_multi_network', false );
			return false;
		}

		$num_sites = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->site}" );
		if ( $num_sites > 1 ) {
			Cache::set( 'is_multi_network', true );
			return true;
		}

		Cache::set( 'is_multi_network', false );
		return false;
	}

	/**
	 * Whether the current site is single user site.
	 *
	 * @return bool
	 */
	public function is_single_user_site() {
		global $wpdb;

		$ret = Cache::get( 'is_single_user_site' );
		if ( null === $ret ) {
			$some_users = get_transient( 'jetpack_is_single_user' );
			if ( false === $some_users ) {
				$some_users = $wpdb->get_var( "SELECT COUNT(*) FROM (SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}capabilities' LIMIT 2) AS someusers" );
				set_transient( 'jetpack_is_single_user', (int) $some_users, 12 * HOUR_IN_SECONDS );
			}
			$ret = 1 === (int) $some_users;
			Cache::set( 'is_single_user_site', $ret );
		}
		return $ret;
	}

	/**
	 * If the site is a local site.
	 *
	 * @since 1.3.0
	 *
	 * @return bool
	 */
	public function is_local_site() {
		$cached = Cache::get( 'is_local_site' );
		if ( null !== $cached ) {
			return $cached;
		}

		$site_url = site_url();

		// Check for localhost and sites using an IP only first.
		// Note: str_contains() is not used here, as wp-includes/compat.php is not loaded in this file.
		$is_local = $site_url && false === strpos( $site_url, '.' );

		// Use Core's environment check, if available.
		if ( 'local' === wp_get_environment_type() ) {
			$is_local = true;
		}

		// Then check for usual usual domains used by local dev tools.
		$known_local = array(
			'#\.local$#i',
			'#\.localhost$#i',
			'#\.test$#i',
			'#\.docksal$#i',      // Docksal.
			'#\.docksal\.site$#i', // Docksal.
			'#\.dev\.cc$#i',       // ServerPress.
			'#\.lndo\.site$#i',    // Lando.
			'#\.ddev\.site$#i',    // DDEV.
			'#^https?://127\.0\.0\.1$#',
		);

		if ( ! $is_local ) {
			foreach ( $known_local as $url ) {
				if ( preg_match( $url, $site_url ) ) {
					$is_local = true;
					break;
				}
			}
		}

		/**
		 * Filters is_local_site check.
		 *
		 * @since 1.3.0
		 *
		 * @param bool $is_local If the current site is a local site.
		 */
		$is_local = apply_filters( 'jetpack_is_local_site', $is_local );

		Cache::set( 'is_local_site', $is_local );
		return $is_local;
	}

	/**
	 * If is a staging site.
	 *
	 * @deprecated since 3.3.0
	 *
	 * @return bool
	 */
	public function is_staging_site() {
		_deprecated_function( __FUNCTION__, '3.3.0', 'in_safe_mode' );
		$cached = Cache::get( 'is_staging_site' );
		if ( null !== $cached ) {
			return $cached;
		}

		/*
		 * Core's wp_get_environment_type allows for a few specific options.
		 * We should default to bowing out gracefully for anything other than production or local.
		 */
		$is_staging = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );

		$known_staging = array(
			'urls'      => array(
				'#\.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
				'#\.staging\.kinsta\.com$#i',                      // Kinsta.com.
				'#\.kinsta\.cloud$#i',                             // Kinsta.com.
				'#\.stage\.site$#i',                               // DreamPress.
				'#\.newspackstaging\.com$#i',                      // Newspack.
				'#^(?!live-)([a-zA-Z0-9-]+)\.pantheonsite\.io$#i', // Pantheon.
				'#\.flywheelsites\.com$#i',                        // Flywheel.
				'#\.flywheelstaging\.com$#i',                      // Flywheel.
				'#\.cloudwaysapps\.com$#i',                        // Cloudways.
				'#\.azurewebsites\.net$#i',                        // Azure.
				'#\.wpserveur\.net$#i',                            // WPServeur.
				'#\-liquidwebsites\.com$#i',                       // Liquidweb.
			),
			'constants' => array(
				'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
				'KINSTA_DEV_ENV',       // Kinsta.com.
				'WPSTAGECOACH_STAGING', // WP Stagecoach.
				'JETPACK_STAGING_MODE', // Generic.
				'WP_LOCAL_DEV',         // Generic.
			),
		);
		/**
		 * Filters the flags of known staging sites.
		 *
		 * @since 1.1.1
		 * @since-jetpack 3.9.0
		 *
		 * @param array $known_staging {
		 *     An array of arrays that each are used to check if the current site is staging.
		 *     @type array $urls      URLs of staging sites in regex to check against site_url.
		 *     @type array $constants PHP constants of known staging/developement environments.
		 *  }
		 */
		$known_staging = apply_filters( 'jetpack_known_staging', $known_staging );

		if ( isset( $known_staging['urls'] ) ) {
			$site_url = site_url();
			foreach ( $known_staging['urls'] as $url ) {
				if ( preg_match( $url, wp_parse_url( $site_url, PHP_URL_HOST ) ) ) {
					$is_staging = true;
					break;
				}
			}
		}

		if ( isset( $known_staging['constants'] ) ) {
			foreach ( $known_staging['constants'] as $constant ) {
				if ( defined( $constant ) && constant( $constant ) ) {
					$is_staging = true;
				}
			}
		}

		// Last, let's check if sync is erroring due to an IDC. If so, set the site to staging mode.
		if ( ! $is_staging && method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
			$is_staging = true;
		}

		/**
		 * Filters is_staging_site check.
		 *
		 * @since 1.1.1
		 * @since-jetpack 3.9.0
		 *
		 * @param bool $is_staging If the current site is a staging site.
		 */
		$is_staging = apply_filters( 'jetpack_is_staging_site', $is_staging );

		Cache::set( 'is_staging_site', $is_staging );
		return $is_staging;
	}

	/**
	 * If the site is in safe mode.
	 *
	 * @since 3.3.0
	 *
	 * @return bool
	 */
	public function in_safe_mode() {
		$cached = Cache::get( 'in_safe_mode' );
		if ( null !== $cached ) {
			return $cached;
		}
		$in_safe_mode = false;
		if ( method_exists( 'Automattic\\Jetpack\\Identity_Crisis', 'validate_sync_error_idc_option' ) && \Automattic\Jetpack\Identity_Crisis::validate_sync_error_idc_option() ) {
			$in_safe_mode = true;
		}
		/**
		 * Filters in_safe_mode check.
		 *
		 * @since 3.3.0
		 *
		 * @param bool $in_safe_mode If the current site is in safe mode.
		 */
		$in_safe_mode = apply_filters( 'jetpack_is_in_safe_mode', $in_safe_mode );

		Cache::set( 'in_safe_mode', $in_safe_mode );
		return $in_safe_mode;
	}

	/**
	 * If the site is a development/staging site.
	 * This is a new version of is_staging_site added to separate safe mode from the legacy staging mode.
	 * This method checks for core WP_ENVIRONMENT_TYPE setting
	 * Using the jetpack_is_development_site filter.
	 *
	 * @since 3.3.0
	 *
	 * @return bool
	 */
	public static function is_development_site() {
		$cached = Cache::get( 'is_development_site' );
		if ( null !== $cached ) {
			return $cached;
		}
		$is_dev_site = ! in_array( wp_get_environment_type(), array( 'production', 'local' ), true );
		/**
		 * Filters is_development_site check.
		 *
		 * @since 3.3.0
		 *
		 * @param bool $is_dev_site If the current site is a staging or dev site.
		 */
		$is_dev_site = apply_filters( 'jetpack_is_development_site', $is_dev_site );

		Cache::set( 'is_development_site', $is_dev_site );
		return $is_dev_site;
	}

	/**
	 * Whether the site is currently onboarding or not.
	 * A site is considered as being onboarded if it currently has an onboarding token.
	 *
	 * @since-jetpack 5.8
	 *
	 * @deprecated since 4.0.0
	 *
	 * @access public
	 * @static
	 *
	 * @return bool True if the site is currently onboarding, false otherwise
	 */
	public function is_onboarding() {
		return \Jetpack_Options::get_option( 'onboarding' ) !== false;
	}

	/**
	 * Whether the site is currently private or not.
	 * On WordPress.com and WoA, sites can be marked as private
	 *
	 * @since 1.16.0
	 *
	 * @return bool True if the site is private.
	 */
	public function is_private_site() {
		$ret = Cache::get( 'is_private_site' );
		if ( null === $ret ) {
			$is_private_site = '-1' === get_option( 'blog_public' );

			/**
			 * Filters the is_private_site check.
			 *
			 * @since 1.16.1
			 *
			 * @param bool $is_private_site True if the site is private.
			 */
			$is_private_site = apply_filters( 'jetpack_is_private_site', $is_private_site );

			Cache::set( 'is_private_site', $is_private_site );
			return $is_private_site;
		}
		return $ret;
	}

	/**
	 * Whether the site is currently unlaunched or not.
	 * On WordPress.com and WoA, sites can be marked as "coming soon", aka unlaunched
	 *
	 * @since 1.16.0
	 *
	 * @return bool True if the site is not launched.
	 */
	public function is_coming_soon() {
		$ret = Cache::get( 'is_coming_soon' );
		if ( null === $ret ) {
			$is_coming_soon = ( function_exists( 'site_is_coming_soon' ) && \site_is_coming_soon() )
				|| get_option( 'wpcom_public_coming_soon' );

			/**
			 * Filters the is_coming_soon check.
			 *
			 * @since 1.16.1
			 *
			 * @param bool $is_coming_soon True if the site is coming soon (i.e. unlaunched).
			 */
			$is_coming_soon = apply_filters( 'jetpack_is_coming_soon', $is_coming_soon );

			Cache::set( 'is_coming_soon', $is_coming_soon );
			return $is_coming_soon;
		}
		return $ret;
	}

	/**
	 * Returns the site slug suffix to be used as part of Calypso URLs.
	 *
	 * Strips http:// or https:// from a url, replaces forward slash with ::.
	 *
	 * @since 1.6.0
	 *
	 * @param string $url Optional. URL to build the site suffix from. Default: Home URL.
	 *
	 * @return string
	 */
	public function get_site_suffix( $url = '' ) {
		// On WordPress.com, site suffixes are a bit different.
		if ( method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
			return WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
		}

		// Grab the 'site_url' option for WoA sites to avoid plugins to interfere with the site
		// identifier (e.g. i18n plugins may change the main url to '<DOMAIN>/<LOCALE>', but we
		// want to exclude the locale since it's not part of the site suffix).
		if ( ( new Host() )->is_woa_site() ) {
			$url = \site_url();
		}

		if ( empty( $url ) ) {
			// WordPress can be installed in subdirectories (e.g. make.wordpress.org/plugins)
			// where the 'site_url' option points to the root domain (e.g. make.wordpress.org)
			// which could collide with another site in the same domain but with WordPress
			// installed in a different subdirectory (e.g. make.wordpress.org/core). To avoid
			// such collision, we identify the site with the 'home_url' option.
			$url = \home_url();
		}

		$url = preg_replace( '#^.*?://#', '', $url );
		$url = str_replace( '/', '::', $url );

		return rtrim( $url, ':' );
	}
}
