Source: classes/SettingsSetupProfile.php

<?php
/**
 * Settings setup profile.
 *
 * @package PoweredCache
 */

namespace PoweredCache;

/**
 * Builds guided setup recommendations for the settings UI.
 *
 * @since 4.0.0
 */
class SettingsSetupProfile {

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

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

	/**
	 * Create an instance.
	 *
	 * @param array $context Runtime context.
	 *
	 * @return SettingsSetupProfile
	 */
	public static function factory( array $context = array() ) {
		return new self( $context );
	}

	/**
	 * Build a lightweight setup profile for guided admin UI recommendations.
	 *
	 * @param array $settings Current settings.
	 *
	 * @return array
	 */
	public function report( array $settings ) {
		$active_plugins  = self::active_plugins();
		$environments    = isset( $this->context['active_environments'] ) && is_array( $this->context['active_environments'] ) ? $this->context['active_environments'] : array();
		$detected        = array();
		$recommendations = array();

		if ( $this->has_plugin( $active_plugins, array( 'woocommerce/woocommerce.php' ) ) ) {
			$detected[]        = $this->detection( 'woocommerce', 'WooCommerce', 'plugin' );
			$recommendations[] = $this->recommendation(
				'woocommerce_safeguards',
				'Review commerce safeguards',
				'Cart, checkout, and account pages are protected automatically. Review exclusions before enabling aggressive JavaScript delay.',
				'advanced',
				'high'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'sitepress-multilingual-cms/sitepress.php',
				'polylang/polylang.php',
				'translatepress-multilingual/index.php',
				'woocommerce-multilingual/wpml-woocommerce.php',
			)
		) ) {
			$detected[]        = $this->detection( 'multilingual', 'Multilingual site', 'site' );
			$recommendations[] = $this->recommendation(
				'multilingual_preload',
				'Confirm multilingual preload',
				'Preload can cover language variants after your public language URLs are confirmed.',
				'preload'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'contact-form-7/wp-contact-form-7.php',
				'fluentform/fluentform.php',
				'forminator/forminator.php',
				'gravityforms/gravityforms.php',
				'jetformbuilder/jet-form-builder.php',
				'ninja-forms/ninja-forms.php',
				'wpforms-lite/wpforms.php',
				'wpforms/wpforms.php',
			)
		) ) {
			$detected[]        = $this->detection( 'forms', 'Forms', 'plugin' );
			$recommendations[] = $this->recommendation(
				'form_script_safeguards',
				'Test key forms after script optimization',
				'Detected form plugins are protected by compatibility rules, but your main forms should still be checked after JavaScript changes.',
				'file_optimization'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'bb-plugin/fl-builder.php',
				'beaver-builder-lite-version/fl-builder.php',
				'bricks/bricks.php',
				'breakdance/plugin.php',
				'elementor/elementor.php',
				'elementor-pro/elementor-pro.php',
				'generateblocks/plugin.php',
				'kadence-blocks/kadence-blocks.php',
				'kadence-pro/kadence-pro.php',
				'oxygen/functions.php',
				'stackable-ultimate-gutenberg-blocks/plugin.php',
				'ultimate-addons-for-gutenberg/ultimate-addons-for-gutenberg.php',
			)
		) ) {
			$detected[]        = $this->detection( 'page_builder', 'Page builder', 'plugin' );
			$recommendations[] = $this->recommendation(
				'page_builder_optimization',
				'Review page builder safeguards',
				'Detected builder scripts are protected by compatibility rules. Review CSS and JavaScript optimization on key templates before launch.',
				'file_optimization'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'complianz-gdpr/complianz-gdpr.php',
				'cookiebot/cookiebot.php',
				'cookie-notice/cookie-notice.php',
				'eu-cookie-law/eu-cookie-law.php',
				'gdpr/gdpr.php',
				'consent-magic-pro/consent-magic-pro.php',
			)
		) ) {
			$detected[]        = $this->detection( 'consent', 'Consent management', 'plugin' );
			$recommendations[] = $this->recommendation(
				'consent_controls',
				'Confirm consent controls',
				'Consent scripts are kept out of delayed JavaScript. Verify the banner and preference controls after enabling script optimizations.',
				'file_optimization'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'revslider/revslider.php',
				'smart-slider-3/smart-slider-3.php',
			)
		) ) {
			$detected[]        = $this->detection( 'slider', 'Slider or hero media', 'plugin' );
			$recommendations[] = $this->recommendation(
				'hero_media',
				'Check hero media after optimization',
				'Slider scripts are protected by compatibility rules. Review the first viewport after lazy-load and JavaScript changes.',
				'media'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'autoptimize/autoptimize.php',
				'perfmatters/perfmatters.php',
				'shortpixel-adaptive-images/short-pixel-ai.php',
			)
		) ) {
			$detected[]        = $this->detection( 'optimizer_overlap', 'Optimization plugin overlap', 'plugin' );
			$recommendations[] = $this->recommendation(
				'optimizer_overlap',
				'Review overlapping optimization layers',
				'Another optimization plugin is active. Keep only one layer responsible for minification, delayed scripts, and image delivery where possible.',
				'file_optimization',
				'high'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'litespeed-cache/litespeed-cache.php',
				'w3-total-cache/w3-total-cache.php',
				'wp-super-cache/wp-cache.php',
				'wp-fastest-cache/wpFastestCache.php',
				'sg-cachepress/sg-cachepress.php',
				'breeze/breeze.php',
			)
		) ) {
			$detected[]        = $this->detection( 'external_cache', 'Existing cache layer', 'plugin' );
			$recommendations[] = $this->recommendation(
				'external_cache',
				'Review active cache layers',
				'Another page cache plugin is active. Avoid running two full-page cache layers unless one is intentionally disabled.',
				'cache',
				'high'
			);
		}

		if ( $this->has_plugin(
			$active_plugins,
			array(
				'a3-lazy-load/a3-lazy-load.php',
				'jetpack-boost/jetpack-boost.php',
				'lazy-load/lazy-load.php',
				'bj-lazy-load/bj-lazy-load.php',
			)
		) ) {
			$detected[]        = $this->detection( 'external_lazy_load', 'Existing lazy-load layer', 'plugin' );
			$recommendations[] = $this->recommendation(
				'external_lazy_load',
				'Review lazy-load ownership',
				'Another lazy-load layer is active. Keep one plugin responsible for lazy-loading unless the front end has been tested carefully.',
				'media'
			);
		}

		if ( $this->cloudflare_detected( $active_plugins ) ) {
			if ( ! in_array( 'cdn:cloudflare', $environments, true ) ) {
				$detected[] = $this->detection( 'cloudflare', 'Cloudflare edge', 'environment' );
			}

			if ( empty( $settings['enable_cloudflare'] ) ) {
				$recommendations[] = $this->recommendation(
					'cloudflare_integration',
					'Connect Cloudflare purge',
					'Cloudflare signals are detected. Connect the integration so cache purges can stay coordinated across WordPress and the edge.',
					'integrations'
				);
			}
		}

		foreach ( $this->environment_detections( $environments ) as $environment ) {
			$detected[] = $environment;
		}

		$managed_host_names = $this->managed_host_names( $environments );

		if ( ! empty( $managed_host_names ) && ! empty( $settings['enable_page_cache'] ) ) {
			$recommendations[] = $this->recommendation(
				'managed_host_cache',
				'Coordinate host cache purges',
				sprintf( 'A managed cache layer was detected on %s. Purge the host cache when testing page cache changes.', implode( ', ', $managed_host_names ) ),
				'cache'
			);
		}

		if ( in_array( 'server:nginx', $environments, true ) && ! empty( $settings['auto_configure_htaccess'] ) ) {
			$recommendations[] = $this->recommendation(
				'nginx_htaccess',
				'Review .htaccess automation',
				'Nginx does not read .htaccess files. Keep automatic .htaccess edits off unless Apache-compatible rewrite handling is also available.',
				'advanced',
				'high'
			);
		}

		if ( empty( $settings['enable_page_cache'] ) ) {
			array_unshift(
				$recommendations,
				$this->recommendation(
					'page_cache',
					'Start with page cache',
					'Enable full-page cache first so anonymous visits get the fastest baseline before advanced optimizations are tuned.',
					'cache',
					'high'
				)
			);
		}

		$object_cache_backends = isset( $this->context['object_cache_backends'] ) && is_array( $this->context['object_cache_backends'] ) ? $this->context['object_cache_backends'] : array();
		if ( ! empty( $object_cache_backends ) && ( empty( $settings['object_cache'] ) || 'off' === $settings['object_cache'] ) ) {
			$recommendations[] = $this->recommendation(
				'object_cache',
				'Consider persistent object cache',
				'This server has a supported object cache backend available for dynamic WordPress data.',
				'cache'
			);
		}

		/**
		 * Filter the guided setup profile returned with settings state.
		 *
		 * @hook powered_cache_settings_setup_profile
		 *
		 * @param {array} $profile  Setup profile.
		 * @param {array} $settings Current settings.
		 * @param {array} $context  Runtime context.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$profile = apply_filters(
			'powered_cache_settings_setup_profile',
			array(
				'detected'        => $this->unique_items( $detected ),
				'plugin_count'    => count( $active_plugins ),
				'recommendations' => array_slice( $this->prioritize_recommendations( $this->unique_items( $recommendations ) ), 0, 5 ),
			),
			$settings,
			$this->context
		);

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

	/**
	 * Return active hosting/runtime environment identifiers.
	 *
	 * @return array
	 */
	public static function active_environments() {
		$environments = array();
		$software     = self::server_value( 'SERVER_SOFTWARE' );

		if ( false !== stripos( $software, 'litespeed' ) ) {
			$environments[] = 'server:litespeed';
		}

		if ( false !== stripos( $software, 'nginx' ) ) {
			$environments[] = 'server:nginx';
		}

		if ( false !== stripos( $software, 'apache' ) ) {
			$environments[] = 'server:apache';
		}

		if ( '' !== self::server_value( 'HTTP_CF_RAY' ) || false !== stripos( self::server_value( 'HTTP_CDN_LOOP' ), 'cloudflare' ) ) {
			$environments[] = 'cdn:cloudflare';
		}

		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';
		}

		/**
		 * Filter active hosting/runtime environments used by guided setup recommendations.
		 *
		 * @hook powered_cache_settings_setup_active_environments
		 *
		 * @param {array} $environments Active environment identifiers.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$environments = apply_filters( 'powered_cache_settings_setup_active_environments', $environments );

		return is_array( $environments ) ? self::normalize_list( $environments ) : array();
	}

	/**
	 * Return active plugin basenames.
	 *
	 * @return array
	 */
	public static function active_plugins() {
		$plugins = array();

		if ( function_exists( 'get_option' ) ) {
			$option_plugins = get_option( 'active_plugins', array() );

			if ( is_array( $option_plugins ) ) {
				$plugins = array_merge( $plugins, $option_plugins );
			}
		}

		if ( function_exists( 'get_site_option' ) ) {
			$network_plugins = get_site_option( 'active_sitewide_plugins', array() );

			if ( is_array( $network_plugins ) ) {
				$plugins = array_merge( $plugins, array_keys( $network_plugins ) );
			}
		}

		/**
		 * Filter active plugin basenames used by guided setup recommendations.
		 *
		 * @hook powered_cache_settings_setup_active_plugins
		 *
		 * @param {array} $plugins Active plugin basenames.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$plugins = apply_filters( 'powered_cache_settings_setup_active_plugins', $plugins );

		return is_array( $plugins ) ? self::normalize_list( $plugins ) : array();
	}

	/**
	 * Determine whether one of the plugin basenames is active.
	 *
	 * @param array $active_plugins Active plugin basenames.
	 * @param array $candidates     Plugin basenames to match.
	 *
	 * @return bool
	 */
	private function has_plugin( array $active_plugins, array $candidates ) {
		foreach ( $candidates as $candidate ) {
			if ( in_array( $candidate, $active_plugins, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Determine whether Cloudflare is detected by plugin or environment.
	 *
	 * @param array $active_plugins Active plugin basenames.
	 *
	 * @return bool
	 */
	private function cloudflare_detected( array $active_plugins ) {
		$active_environments = isset( $this->context['active_environments'] ) && is_array( $this->context['active_environments'] ) ? $this->context['active_environments'] : array();

		return in_array( 'cdn:cloudflare', $active_environments, true ) || $this->has_plugin( $active_plugins, array( 'cloudflare/cloudflare.php' ) );
	}

	/**
	 * Return setup detections for hosting/runtime environments.
	 *
	 * @param array $environments Active environment identifiers.
	 *
	 * @return array
	 */
	private function environment_detections( array $environments ) {
		$labels     = $this->environment_labels();
		$detections = array();

		foreach ( $environments as $environment ) {
			if ( ! isset( $labels[ $environment ] ) ) {
				continue;
			}

			$detections[] = $this->detection( $this->environment_key( $environment ), $labels[ $environment ], 'environment' );
		}

		return $detections;
	}

	/**
	 * Return readable managed host names from active environments.
	 *
	 * @param array $environments Active environment identifiers.
	 *
	 * @return array
	 */
	private function managed_host_names( array $environments ) {
		$labels = $this->managed_host_labels();
		$names  = array();

		foreach ( $environments as $environment ) {
			if ( isset( $labels[ $environment ] ) ) {
				$names[] = $labels[ $environment ];
			}
		}

		return $names;
	}

	/**
	 * Return environment labels for setup chips.
	 *
	 * @return array
	 */
	private function environment_labels() {
		return array(
			'cdn:cloudflare'   => 'Cloudflare edge',
			'host:kinsta'      => 'Kinsta',
			'host:pantheon'    => 'Pantheon',
			'host:wp-engine'   => 'WP Engine',
			'server:apache'    => 'Apache',
			'server:litespeed' => 'LiteSpeed',
			'server:nginx'     => 'Nginx',
		);
	}

	/**
	 * Return a stable setup key for one environment identifier.
	 *
	 * @param string $environment Environment identifier.
	 *
	 * @return string
	 */
	private function environment_key( $environment ) {
		if ( 'cdn:cloudflare' === $environment ) {
			return 'cloudflare';
		}

		return str_replace( array( ':', '-' ), '_', $environment );
	}

	/**
	 * Return managed host labels.
	 *
	 * @return array
	 */
	private function managed_host_labels() {
		return array(
			'host:kinsta'    => 'Kinsta',
			'host:pantheon'  => 'Pantheon',
			'host:wp-engine' => 'WP Engine',
		);
	}

	/**
	 * Build a setup detection item.
	 *
	 * @param string $key   Detection key.
	 * @param string $label Detection label.
	 * @param string $type  Detection type.
	 *
	 * @return array
	 */
	private function detection( $key, $label, $type ) {
		return array(
			'key'   => $key,
			'label' => $label,
			'type'  => $type,
		);
	}

	/**
	 * Build a setup recommendation item.
	 *
	 * @param string $key      Recommendation key.
	 * @param string $label    Recommendation label.
	 * @param string $message  Recommendation message.
	 * @param string $section  Settings section key.
	 * @param string $priority Recommendation priority.
	 *
	 * @return array
	 */
	private function recommendation( $key, $label, $message, $section, $priority = 'normal' ) {
		return array(
			'key'      => $key,
			'label'    => $label,
			'message'  => $message,
			'priority' => $priority,
			'section'  => $section,
		);
	}

	/**
	 * Return unique setup items by key.
	 *
	 * @param array $items Setup items.
	 *
	 * @return array
	 */
	private function unique_items( array $items ) {
		$unique = array();
		$seen   = array();

		foreach ( $items as $item ) {
			if ( empty( $item['key'] ) || isset( $seen[ $item['key'] ] ) ) {
				continue;
			}

			$unique[]            = $item;
			$seen[ $item['key'] ] = true;
		}

		return $unique;
	}

	/**
	 * Keep high-impact recommendations first while preserving relative order.
	 *
	 * @param array $recommendations Recommendation items.
	 *
	 * @return array
	 */
	private function prioritize_recommendations( array $recommendations ) {
		$high   = array();
		$normal = array();

		foreach ( $recommendations as $recommendation ) {
			if ( isset( $recommendation['priority'] ) && 'high' === $recommendation['priority'] ) {
				$high[] = $recommendation;
				continue;
			}

			$normal[] = $recommendation;
		}

		return array_merge( $high, $normal );
	}

	/**
	 * Return normalized unique scalar values.
	 *
	 * @param array $values Values.
	 *
	 * @return array
	 */
	private static function normalize_list( array $values ) {
		$normalized = array();

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

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

			if ( '' === $value || in_array( $value, $normalized, true ) ) {
				continue;
			}

			$normalized[] = $value;
		}

		return $normalized;
	}

	/**
	 * Return a sanitized server value.
	 *
	 * @param string $key Server key.
	 *
	 * @return string
	 */
	private static function server_value( $key ) {
		if ( empty( $_SERVER[ $key ] ) || ! is_scalar( $_SERVER[ $key ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			return '';
		}

		$value = $_SERVER[ $key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash

		if ( function_exists( 'wp_unslash' ) ) {
			$value = wp_unslash( $value );
		}

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