Source: classes/SettingsRestController.php

<?php
/**
 * Settings REST controller.
 *
 * @package PoweredCache
 */

namespace PoweredCache;

/**
 * Exposes settings schema payloads for admin UI consumers.
 *
 * @since 4.0.0
 */
class SettingsRestController {

	const REST_NAMESPACE = 'powered-cache/v1';
	const STATE_FORMAT   = 'powered-cache-settings-state';

	/**
	 * Return an instance of the current class.
	 *
	 * @return SettingsRestController
	 */
	public static function factory() {
		static $instance = false;

		if ( ! $instance ) {
			$instance = new self();
			$instance->setup();
		}

		return $instance;
	}

	/**
	 * Setup hooks.
	 */
	public function setup() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register settings routes.
	 */
	public function register_routes() {
		register_rest_route(
			self::REST_NAMESPACE,
			'/settings',
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'get_settings' ),
					'permission_callback' => array( $this, 'can_read_manifest' ),
				),
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'update_settings' ),
					'permission_callback' => array( $this, 'can_read_manifest' ),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/settings/manifest',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_manifest' ),
				'permission_callback' => array( $this, 'can_read_manifest' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/settings/recommended',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'apply_recommended_settings' ),
				'permission_callback' => array( $this, 'can_read_manifest' ),
			)
		);
	}

	/**
	 * Determine whether the current user can read the settings manifest.
	 *
	 * @return bool
	 */
	public function can_read_manifest() {
		return \PoweredCache\Utils\can_control_all_settings();
	}

	/**
	 * Return the settings manifest.
	 *
	 * @return array
	 */
	public function get_manifest() {
		return SettingsManifest::build( $this->settings_context(), false );
	}

	/**
	 * Return current settings for admin UI consumers.
	 *
	 * @return array
	 */
	public function get_settings() {
		$context = $this->settings_context();

		$repository = SettingsRepository::factory(
			POWERED_CACHE_IS_NETWORK,
			$context
		);
		$settings   = $repository->all();

		return array(
			'format'             => self::STATE_FORMAT,
			'format_version'     => SettingsManifest::FORMAT_VERSION,
			'plugin_version'     => defined( 'POWERED_CACHE_VERSION' ) ? POWERED_CACHE_VERSION : '',
			'settings'           => SettingsRepository::redact_sensitive( $settings ),
			'sensitive_settings' => SettingsRepository::sensitive_value_status( $settings ),
			'validation'         => SettingsValidator::factory( $context )->report( $settings ),
			'system_status'      => SystemStatus::factory( $settings, $context )->report(),
			'setup_profile'      => $this->setup_profile( $settings, $context ),
		);
	}

	/**
	 * Update settings from REST payload.
	 *
	 * @param mixed $request REST request.
	 *
	 * @return array
	 */
	public function update_settings( $request ) {
		$context = $this->settings_context();

		$repository = SettingsRepository::factory(
			POWERED_CACHE_IS_NETWORK,
			$context
		);

		$old_settings = $repository->all();
		$changes      = SettingsRepository::settings_from_payload( $this->get_request_payload( $request ) );
		$settings     = $this->prepare_update_settings( $changes, $old_settings );
		$save_service = SettingsSaveService::factory( $repository, POWERED_CACHE_IS_NETWORK );
		$settings     = $save_service->save( $settings, $old_settings );

		return array(
			'format'             => self::STATE_FORMAT,
			'format_version'     => SettingsManifest::FORMAT_VERSION,
			'plugin_version'     => defined( 'POWERED_CACHE_VERSION' ) ? POWERED_CACHE_VERSION : '',
			'settings'           => SettingsRepository::redact_sensitive( $settings ),
			'sensitive_settings' => SettingsRepository::sensitive_value_status( $settings ),
			'validation'         => SettingsValidator::factory( $context )->report( $settings ),
			'system_status'      => SystemStatus::factory( $settings, $context )->report(),
			'setup_profile'      => $this->setup_profile( $settings, $context ),
		);
	}

	/**
	 * Apply the recommended setup profile.
	 *
	 * @return array
	 */
	public function apply_recommended_settings() {
		$context = $this->settings_context();

		$repository = SettingsRepository::factory(
			POWERED_CACHE_IS_NETWORK,
			$context
		);

		$old_settings = $repository->all();
		$changes      = SettingsSchema::recommended( $context );
		$settings     = $this->prepare_update_settings( $changes, $old_settings );
		$save_service = SettingsSaveService::factory( $repository, POWERED_CACHE_IS_NETWORK );
		$settings     = $save_service->save( $settings, $old_settings );
		$this->mark_site_defaults_applied();

		return array(
			'format'             => self::STATE_FORMAT,
			'format_version'     => SettingsManifest::FORMAT_VERSION,
			'plugin_version'     => defined( 'POWERED_CACHE_VERSION' ) ? POWERED_CACHE_VERSION : '',
			'settings'           => SettingsRepository::redact_sensitive( $settings ),
			'sensitive_settings' => SettingsRepository::sensitive_value_status( $settings ),
			'validation'         => SettingsValidator::factory( $context )->report( $settings ),
			'system_status'      => SystemStatus::factory( $settings, $context )->report(),
			'setup_profile'      => $this->setup_profile( $settings, $context, true ),
			'applied'            => array_keys( $changes ),
		);
	}

	/**
	 * Prepare partial update payload before storage.
	 *
	 * @param array $changes Changed settings.
	 * @param array $current Current settings.
	 *
	 * @return array
	 */
	public function prepare_update_settings( array $changes, array $current ) {
		$settings = array_merge( $current, $changes );

		foreach ( SettingsRepository::sensitive_keys() as $key ) {
			if ( ! array_key_exists( $key, $changes ) ) {
				continue;
			}

			if ( '' === $changes[ $key ] ) {
				$settings[ $key ] = isset( $current[ $key ] ) ? $current[ $key ] : '';
				continue;
			}

			if ( null === $changes[ $key ] ) {
				$settings[ $key ] = '';
				continue;
			}

			if ( in_array( $key, array( 'cloudflare_api_key', 'cloudflare_api_token', 'sucuri_api_key', 'sucuri_api_secret' ), true ) ) {
				$settings[ $key ] = ( new Encryption() )->encrypt( $changes[ $key ] );
			}
		}

		$settings = SettingsSchema::enforce_editable( $settings, $current );

		/**
		 * Filters sanitized options saved through the settings REST API.
		 *
		 * @hook powered_cache_sanitized_options
		 *
		 * @param {array} $settings Sanitized settings.
		 * @param {array} $changes  Raw changed settings.
		 *
		 * @return {array} New value.
		 */
		return apply_filters( 'powered_cache_sanitized_options', $settings, $changes );
	}

	/**
	 * Read request payload.
	 *
	 * @param mixed $request REST request.
	 *
	 * @return array
	 */
	private function get_request_payload( $request ) {
		if ( is_array( $request ) ) {
			return $request;
		}

		if ( is_object( $request ) && method_exists( $request, 'get_json_params' ) ) {
			$params = $request->get_json_params();

			if ( is_array( $params ) ) {
				return $params;
			}
		}

		if ( is_object( $request ) && method_exists( $request, 'get_params' ) ) {
			$params = $request->get_params();

			return is_array( $params ) ? $params : array();
		}

		return array();
	}

	/**
	 * Build runtime context for schema defaults and settings validation.
	 *
	 * @return array
	 */
	private function settings_context() {
		global $is_apache;

		$context = array(
			'is_apache'                => (bool) $is_apache,
			'is_local_site'            => function_exists( 'site_url' ) && function_exists( '\PoweredCache\Utils\is_local_site' ) && \PoweredCache\Utils\is_local_site(),
			'wp_cache_enabled'         => defined( 'WP_CACHE' ) && true === WP_CACHE,
			'page_cache_loaded'        => defined( 'POWERED_CACHE_PAGE_CACHING' ) && true === POWERED_CACHE_PAGE_CACHING,
			'page_cache_has_problem'   => defined( 'POWERED_CACHE_PAGE_CACHING_HAS_PROBLEM' ) && POWERED_CACHE_PAGE_CACHING_HAS_PROBLEM,
			'object_cache_has_problem' => defined( 'POWERED_OBJECT_CACHE_HAS_PROBLEM' ) && POWERED_OBJECT_CACHE_HAS_PROBLEM,
			'active_plugins'           => SettingsSetupProfile::active_plugins(),
			'active_environments'      => SettingsSetupProfile::active_environments(),
		);

		if ( function_exists( '\PoweredCache\Utils\get_available_object_caches' ) ) {
			$context['object_cache_backends'] = \PoweredCache\Utils\get_available_object_caches();
		}

		if ( defined( 'WP_CONTENT_DIR' ) ) {
			$context['object_cache_dropin_exists'] = file_exists( rtrim( WP_CONTENT_DIR, '/\\' ) . '/object-cache.php' );
		}

		if ( $context['is_apache'] && function_exists( 'get_home_path' ) ) {
			$htaccess_file                = rtrim( get_home_path(), '/\\' ) . '/.htaccess';
			$context['htaccess_exists']   = file_exists( $htaccess_file );
			$context['htaccess_writable'] = is_writable( $htaccess_file );
		}

		/**
		 * Filter settings validation runtime context.
		 *
		 * @hook powered_cache_settings_validation_context
		 *
		 * @param {array} $context Runtime context.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$context = apply_filters( 'powered_cache_settings_validation_context', $context );

		return is_array( $context ) ? $context : array();
	}

	/**
	 * Build setup profile state for the settings app.
	 *
	 * @param array     $settings Settings payload.
	 * @param array     $context Runtime context.
	 * @param bool|null $site_defaults_applied Optional known completion state.
	 *
	 * @return array
	 */
	private function setup_profile( array $settings, array $context, $site_defaults_applied = null ) {
		$profile = SettingsSetupProfile::factory( $context )->report( $settings );

		$profile['site_defaults_applied'] = null === $site_defaults_applied
			? $this->site_defaults_applied()
			: (bool) $site_defaults_applied;

		return $profile;
	}

	/**
	 * Determine whether site-aware defaults were applied before.
	 *
	 * @return bool
	 */
	private function site_defaults_applied() {
		if ( POWERED_CACHE_IS_NETWORK ) {
			return (bool) get_site_option( \PoweredCache\Constants\SITE_DEFAULTS_APPLIED_OPTION, false );
		}

		return (bool) get_option( \PoweredCache\Constants\SITE_DEFAULTS_APPLIED_OPTION, false );
	}

	/**
	 * Persist completion of the one-time site-aware defaults action.
	 *
	 * @return void
	 */
	private function mark_site_defaults_applied() {
		if ( POWERED_CACHE_IS_NETWORK ) {
			update_site_option( \PoweredCache\Constants\SITE_DEFAULTS_APPLIED_OPTION, true );
			return;
		}

		update_option( \PoweredCache\Constants\SITE_DEFAULTS_APPLIED_OPTION, true, false );
	}
}