Source: classes/SystemStatus.php

<?php
/**
 * System status report.
 *
 * @package PoweredCache
 */

namespace PoweredCache;

/**
 * Builds admin-facing runtime checks for the settings app.
 *
 * @since 4.0.0
 */
class SystemStatus {

	const STATUS_GOOD    = 'good';
	const STATUS_WARNING = 'warning';
	const STATUS_INFO    = 'info';
	const STATUS_IDLE    = 'idle';

	/**
	 * Settings payload.
	 *
	 * @var array
	 */
	private $settings;

	/**
	 * Runtime context.
	 *
	 * @var array
	 */
	private $context;

	/**
	 * Constructor.
	 *
	 * @param array $settings Settings payload.
	 * @param array $context Runtime context.
	 */
	public function __construct( array $settings, array $context = array() ) {
		$this->settings = $settings;
		$this->context  = $context;
	}

	/**
	 * Create a report instance.
	 *
	 * @param array $settings Settings payload.
	 * @param array $context Runtime context.
	 *
	 * @return SystemStatus
	 */
	public static function factory( array $settings, array $context = array() ) {
		return new self( $settings, $context );
	}

	/**
	 * Return a normalized status report.
	 *
	 * @return array
	 */
	public function report() {
		$checks = array(
			$this->page_cache_check(),
			$this->managed_host_cache_check(),
			$this->cache_storage_check(),
			$this->object_cache_check(),
			$this->action_scheduler_check(),
			$this->cron_check(),
			$this->rest_api_check(),
		);

		/**
		 * Filter settings system status checks.
		 *
		 * @hook powered_cache_system_status_checks
		 *
		 * @param {array} $checks Runtime status checks.
		 * @param {array} $settings Settings payload.
		 * @param {array} $context Runtime context.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$checks = apply_filters( 'powered_cache_system_status_checks', $checks, $this->settings, $this->context );
		$checks = array_values( array_filter( array_map( array( $this, 'normalize_check' ), is_array( $checks ) ? $checks : array() ) ) );

		return array(
			'status'  => $this->summary_status( $checks ),
			'counts'  => $this->counts( $checks ),
			'checks'  => $checks,
			'updated' => time(),
		);
	}

	/**
	 * Return the page cache readiness check.
	 *
	 * @return array
	 */
	private function page_cache_check() {
		if ( empty( $this->settings['enable_page_cache'] ) ) {
			return $this->check(
				'page_cache',
				self::STATUS_IDLE,
				'Page cache',
				'Page cache is disabled.',
				'Enable page cache when the site is ready to serve cached HTML.',
				'cache'
			);
		}

		if ( $this->context_flag_is( 'wp_cache_enabled', false ) ) {
			return $this->check(
				'page_cache',
				self::STATUS_WARNING,
				'Page cache',
				'WP_CACHE is not enabled.',
				'Use Fix configuration in Tools to add WP_CACHE to wp-config.php, or add define( "WP_CACHE", true ) manually.',
				'cache'
			);
		}

		if ( $this->context_flag_is( 'page_cache_loaded', false ) || $this->context_flag_is( 'page_cache_has_problem', true ) ) {
			return $this->check(
				'page_cache',
				self::STATUS_WARNING,
				'Page cache',
				'The page cache drop-in is not active.',
				'Use Fix configuration in Tools to recreate the configuration files, then verify advanced-cache.php is writable.',
				'cache'
			);
		}

		return $this->check(
			'page_cache',
			self::STATUS_GOOD,
			'Page cache',
			'Cached HTML delivery is ready.',
			'Anonymous visits can be served from the page cache.',
			'cache'
		);
	}

	/**
	 * Return managed hosting cache guidance.
	 *
	 * @return array
	 */
	private function managed_host_cache_check() {
		if ( empty( $this->settings['enable_page_cache'] ) ) {
			return array();
		}

		$hosts = array_intersect(
			$this->active_environments(),
			array(
				'host:kinsta',
				'host:wp-engine',
				'host:pantheon',
			)
		);

		if ( empty( $hosts ) ) {
			return array();
		}

		$labels = array(
			'host:kinsta'    => 'Kinsta',
			'host:wp-engine' => 'WP Engine',
			'host:pantheon'  => 'Pantheon',
		);
		$names  = array();

		foreach ( $hosts as $host ) {
			if ( isset( $labels[ $host ] ) ) {
				$names[] = $labels[ $host ];
			}
		}

		return $this->check(
			'managed_host_cache',
			self::STATUS_INFO,
			'Managed host cache',
			'Hosting cache layer detected.',
			sprintf( 'Purge %s cache when testing page cache changes so stale platform responses do not hide the result.', implode( ', ', $names ) ),
			'cache'
		);
	}

	/**
	 * Return the cache storage readiness check.
	 *
	 * @return array
	 */
	private function cache_storage_check() {
		$cache_dir = function_exists( '\PoweredCache\Utils\get_cache_dir' ) ? \PoweredCache\Utils\get_cache_dir() : '';
		$writable  = $this->path_is_writable( $cache_dir );

		if ( ! $writable ) {
			return $this->check(
				'cache_storage',
				self::STATUS_WARNING,
				'Cache storage',
				'Cache directory is not writable.',
				'Make sure wp-content/cache can be created and written by PHP.',
				'misc',
				$cache_dir
			);
		}

		return $this->check(
			'cache_storage',
			self::STATUS_GOOD,
			'Cache storage',
			'Cache directory is writable.',
			'Powered Cache can store generated page and optimization files.',
			'misc',
			$cache_dir
		);
	}

	/**
	 * Return the object cache readiness check.
	 *
	 * @return array
	 */
	private function object_cache_check() {
		$backend = isset( $this->settings['object_cache'] ) ? (string) $this->settings['object_cache'] : 'off';

		if ( '' === $backend || 'off' === $backend ) {
			return $this->check(
				'object_cache',
				self::STATUS_IDLE,
				'Object cache',
				'Persistent object cache is off.',
				'Only supported PHP drivers are shown when this server can use them.',
				'cache'
			);
		}

		$available = isset( $this->context['object_cache_backends'] ) && is_array( $this->context['object_cache_backends'] ) ? $this->context['object_cache_backends'] : array();

		if ( ! in_array( $backend, $available, true ) ) {
			return $this->check(
				'object_cache',
				self::STATUS_WARNING,
				'Object cache',
				'Selected object cache driver is not available.',
				'Install the matching PHP extension or switch the backend off.',
				'cache'
			);
		}

		if ( $this->context_flag_is( 'object_cache_dropin_exists', false ) || $this->context_flag_is( 'object_cache_has_problem', true ) ) {
			return $this->check(
				'object_cache',
				self::STATUS_WARNING,
				'Object cache',
				'Object cache drop-in needs attention.',
				'Use Fix configuration in Tools to recreate object-cache.php, then verify the file is writable.',
				'cache'
			);
		}

		return $this->check(
			'object_cache',
			self::STATUS_GOOD,
			'Object cache',
			ucfirst( $backend ) . ' object cache is configured.',
			'Persistent object caching can be used for dynamic WordPress data.',
			'cache'
		);
	}

	/**
	 * Return the async scheduler readiness check.
	 *
	 * @return array
	 */
	private function action_scheduler_check() {
		$ready = isset( $this->context['action_scheduler_ready'] )
			? (bool) $this->context['action_scheduler_ready']
			: (
				function_exists( 'as_enqueue_async_action' )
				&& class_exists( '\ActionScheduler' )
				&& is_callable( array( '\ActionScheduler', 'is_initialized' ) )
				&& \ActionScheduler::is_initialized( __METHOD__ )
			);

		if ( ! $ready ) {
			return $this->check(
				'action_scheduler',
				self::STATUS_WARNING,
				'Background jobs',
				'Action Scheduler is not ready yet.',
				'Background cache, preload, and optimization queues may wait until Action Scheduler initializes.',
				'misc'
			);
		}

		return $this->check(
			'action_scheduler',
			self::STATUS_GOOD,
			'Background jobs',
			'Action Scheduler is ready.',
			'Async cache and optimization jobs can be queued reliably.',
			'misc'
		);
	}

	/**
	 * Return the cron readiness check.
	 *
	 * @return array
	 */
	private function cron_check() {
		if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
			return $this->check(
				'wp_cron',
				self::STATUS_INFO,
				'WP-Cron',
				'Built-in WP-Cron is disabled.',
				'This is fine when a real server cron runs wp-cron.php regularly.',
				'misc'
			);
		}

		return $this->check(
			'wp_cron',
			self::STATUS_GOOD,
			'WP-Cron',
			'WP-Cron is available.',
			'Scheduled maintenance and background queues have a default runner.',
			'misc'
		);
	}

	/**
	 * Return the REST API readiness check.
	 *
	 * @return array
	 */
	private function rest_api_check() {
		if ( ! function_exists( 'rest_url' ) ) {
			return $this->check(
				'rest_api',
				self::STATUS_WARNING,
				'REST API',
				'REST API helpers are unavailable.',
				'Settings, status refreshes, and service actions require WordPress REST API support.',
				'misc'
			);
		}

		return $this->check(
			'rest_api',
			self::STATUS_GOOD,
			'REST API',
			'REST API is available.',
			'The settings app can save changes and refresh runtime status without page reloads.',
			'misc'
		);
	}

	/**
	 * Build one check.
	 *
	 * @param string $code Check code.
	 * @param string $status Check status.
	 * @param string $label Check label.
	 * @param string $message Short message.
	 * @param string $description Longer description.
	 * @param string $section Settings section key.
	 * @param string $detail Optional detail.
	 *
	 * @return array
	 */
	private function check( $code, $status, $label, $message, $description, $section = '', $detail = '' ) {
		return array(
			'code'        => $code,
			'status'      => $status,
			'label'       => $label,
			'message'     => $message,
			'description' => $description,
			'section'     => $section,
			'detail'      => $detail,
		);
	}

	/**
	 * Normalize one check for REST output.
	 *
	 * @param mixed $check Check payload.
	 *
	 * @return array
	 */
	private function normalize_check( $check ) {
		if ( ! is_array( $check ) || empty( $check['code'] ) ) {
			return array();
		}

		$status = isset( $check['status'] ) ? (string) $check['status'] : self::STATUS_INFO;

		if ( ! in_array( $status, array( self::STATUS_GOOD, self::STATUS_WARNING, self::STATUS_INFO, self::STATUS_IDLE ), true ) ) {
			$status = self::STATUS_INFO;
		}

		return array(
			'code'        => $this->sanitize_key( $check['code'] ),
			'status'      => $status,
			'label'       => isset( $check['label'] ) ? $this->sanitize_text( $check['label'] ) : '',
			'message'     => isset( $check['message'] ) ? $this->sanitize_text( $check['message'] ) : '',
			'description' => isset( $check['description'] ) ? $this->sanitize_text( $check['description'] ) : '',
			'section'     => isset( $check['section'] ) ? $this->sanitize_key( $check['section'] ) : '',
			'detail'      => isset( $check['detail'] ) ? $this->sanitize_text( $check['detail'] ) : '',
		);
	}

	/**
	 * Return summary counts.
	 *
	 * @param array $checks Checks.
	 *
	 * @return array
	 */
	private function counts( array $checks ) {
		$counts = array(
			self::STATUS_GOOD    => 0,
			self::STATUS_WARNING => 0,
			self::STATUS_INFO    => 0,
			self::STATUS_IDLE    => 0,
		);

		foreach ( $checks as $check ) {
			if ( isset( $counts[ $check['status'] ] ) ) {
				$counts[ $check['status'] ]++;
			}
		}

		return $counts;
	}

	/**
	 * Return summary status.
	 *
	 * @param array $checks Checks.
	 *
	 * @return string
	 */
	private function summary_status( array $checks ) {
		$counts = $this->counts( $checks );

		if ( $counts[ self::STATUS_WARNING ] > 0 ) {
			return self::STATUS_WARNING;
		}

		if ( $counts[ self::STATUS_INFO ] > 0 ) {
			return self::STATUS_INFO;
		}

		return self::STATUS_GOOD;
	}

	/**
	 * Determine whether a known runtime context flag matches an expected value.
	 *
	 * @param string $flag     Context flag.
	 * @param bool   $expected Expected value.
	 *
	 * @return bool
	 */
	private function context_flag_is( $flag, $expected ) {
		return array_key_exists( $flag, $this->context ) && (bool) $this->context[ $flag ] === (bool) $expected;
	}

	/**
	 * Return active hosting/runtime environment identifiers.
	 *
	 * @return array
	 */
	private function active_environments() {
		if ( isset( $this->context['active_environments'] ) && is_array( $this->context['active_environments'] ) ) {
			return $this->normalize_list( $this->context['active_environments'] );
		}

		$environments = array();

		if ( defined( 'KINSTAMU_VERSION' ) ) {
			$environments[] = 'host:kinsta';
		}

		if ( defined( 'WPE_APIKEY' ) || defined( 'WPE_CLUSTER_ID' ) || defined( 'PWP_NAME' ) ) {
			$environments[] = 'host:wp-engine';
		}

		if ( defined( 'PANTHEON_ENVIRONMENT' ) ) {
			$environments[] = 'host:pantheon';
		}

		return $this->normalize_list( $environments );
	}

	/**
	 * Normalize a scalar list.
	 *
	 * @param array $values Values.
	 *
	 * @return array
	 */
	private function normalize_list( array $values ) {
		$normalized = array();

		foreach ( $values as $value ) {
			if ( ! is_scalar( $value ) ) {
				continue;
			}

			$value = trim( (string) $value );

			if ( '' !== $value ) {
				$normalized[] = $value;
			}
		}

		return array_values( array_unique( $normalized ) );
	}

	/**
	 * Determine whether a path or its parent can be written.
	 *
	 * @param string $path Path.
	 *
	 * @return bool
	 */
	private function path_is_writable( $path ) {
		$path = (string) $path;

		if ( '' === $path ) {
			return false;
		}

		if ( file_exists( $path ) ) {
			return is_writable( $path );
		}

		$parent = dirname( rtrim( $path, '/\\' ) );

		return $parent && file_exists( $parent ) && is_writable( $parent );
	}

	/**
	 * Sanitize a REST-safe key.
	 *
	 * @param mixed $value Value.
	 *
	 * @return string
	 */
	private function sanitize_key( $value ) {
		if ( function_exists( 'sanitize_key' ) ) {
			return sanitize_key( $value );
		}

		return strtolower( preg_replace( '/[^a-zA-Z0-9_\-]/', '', (string) $value ) );
	}

	/**
	 * Sanitize one line of admin text.
	 *
	 * @param mixed $value Value.
	 *
	 * @return string
	 */
	private function sanitize_text( $value ) {
		if ( function_exists( 'sanitize_text_field' ) ) {
			return sanitize_text_field( $value );
		}

		if ( function_exists( 'wp_strip_all_tags' ) ) {
			return trim( wp_strip_all_tags( (string) $value ) );
		}

		return trim( strip_tags( (string) $value ) );
	}
}