Source: classes/CompatibilityRules.php

<?php
/**
 * Compatibility rules registry.
 *
 * @package PoweredCache
 */

namespace PoweredCache;

/**
 * Loads versioned compatibility rules for optimizer safeguards.
 *
 * @since 4.0.0
 */
class CompatibilityRules {

	const FORMAT           = 'powered-cache-compatibility-rules';
	const FORMAT_VERSION   = '1.0';
	const REMOTE_CACHE_KEY = 'powered_cache_compatibility_rules_remote';
	const REMOTE_MAX_BYTES = 200000;

	/**
	 * Registry file path.
	 *
	 * @var string
	 */
	private $file;

	/**
	 * Decoded registry payload.
	 *
	 * @var array|null
	 */
	private $registry;

	/**
	 * Active plugin basenames.
	 *
	 * @var array|null
	 */
	private $active_plugins;

	/**
	 * Active hosting/runtime environment identifiers.
	 *
	 * @var array|null
	 */
	private $active_environments;

	/**
	 * Constructor.
	 *
	 * @param string|null $file Registry file path.
	 */
	public function __construct( $file = null ) {
		$file = null === $file ? self::default_file() : (string) $file;

		/**
		 * Filter the compatibility rules registry file.
		 *
		 * @hook powered_cache_compatibility_rules_file
		 *
		 * @param {string} $file Registry file path.
		 *
		 * @return {string} New value.
		 *
		 * @since 4.0.0
		 */
		$this->file = (string) apply_filters( 'powered_cache_compatibility_rules_file', $file );
	}

	/**
	 * Create a registry instance.
	 *
	 * @param string|null $file Registry file path.
	 *
	 * @return CompatibilityRules
	 */
	public static function factory( $file = null ) {
		return new self( $file );
	}

	/**
	 * Register runtime filters.
	 */
	public function setup() {
		add_filter( 'powered_cache_defer_exclusions', array( $this, 'add_defer_exclusions' ) );
		add_filter( 'powered_cache_delay_exclusions', array( $this, 'add_delay_exclusions' ) );
		add_filter( 'powered_cache_fo_excluded_css_files', array( $this, 'add_file_optimizer_css_exclusions' ) );
		add_filter( 'powered_cache_fo_excluded_js_files', array( $this, 'add_file_optimizer_js_exclusions' ) );
		add_filter( 'powered_cache_lazy_load_exclusions', array( $this, 'add_lazy_load_exclusions' ) );
	}

	/**
	 * Add default defer exclusions.
	 *
	 * @param array $exclusions Current exclusions.
	 *
	 * @return array
	 */
	public function add_defer_exclusions( array $exclusions ) {
		return $this->append_unique( $exclusions, $this->rules( 'defer_exclusions' ) );
	}

	/**
	 * Add default delay exclusions.
	 *
	 * @param array $exclusions Current exclusions.
	 *
	 * @return array
	 */
	public function add_delay_exclusions( array $exclusions ) {
		return $this->append_unique( $exclusions, $this->rules( 'delay_exclusions' ) );
	}

	/**
	 * Add default CSS optimizer exclusions.
	 *
	 * @param array $exclusions Current exclusions.
	 *
	 * @return array
	 */
	public function add_file_optimizer_css_exclusions( array $exclusions ) {
		return $this->append_unique( $exclusions, $this->rules( 'file_optimizer_css_exclusions' ) );
	}

	/**
	 * Add default JS optimizer exclusions.
	 *
	 * @param array $exclusions Current exclusions.
	 *
	 * @return array
	 */
	public function add_file_optimizer_js_exclusions( array $exclusions ) {
		return $this->append_unique( $exclusions, $this->rules( 'file_optimizer_js_exclusions' ) );
	}

	/**
	 * Add default lazy-load exclusions.
	 *
	 * @param array $exclusions Current exclusions.
	 *
	 * @return array
	 */
	public function add_lazy_load_exclusions( array $exclusions ) {
		return $this->append_unique( $exclusions, $this->rules( 'lazy_load_exclusions' ) );
	}

	/**
	 * Return rules from one registry bucket.
	 *
	 * @param string $bucket Rule bucket.
	 *
	 * @return array
	 */
	public function rules( $bucket ) {
		$registry = $this->registry();

		$rules = array();

		if ( ! empty( $registry['rules'][ $bucket ] ) && is_array( $registry['rules'][ $bucket ] ) ) {
			$rules = $this->normalize_rules( $registry['rules'][ $bucket ] );
		}

		return $this->append_unique( $rules, $this->conditional_rules( $registry, $bucket ) );
	}

	/**
	 * Return settings issues from the compatibility registry.
	 *
	 * @param array $settings Current settings.
	 *
	 * @return array
	 */
	public function settings_issues( array $settings ) {
		$registry = $this->registry();
		$issues   = array();

		if ( ! empty( $registry['settings_issues'] ) && is_array( $registry['settings_issues'] ) ) {
			$issues = $this->setting_issues_from_items( $registry['settings_issues'], $settings );
		}

		return $this->append_unique_issues( $issues, $this->conditional_setting_issues( $registry, $settings ) );
	}

	/**
	 * Return the decoded registry payload.
	 *
	 * @return array
	 */
	public function registry() {
		if ( null !== $this->registry ) {
			return $this->registry;
		}

		$this->registry = array();

		if ( ! is_readable( $this->file ) ) {
			return $this->registry;
		}

		$contents = file_get_contents( $this->file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$registry = json_decode( $contents, true );

		if ( ! $this->is_valid_registry( $registry ) ) {
			return $this->registry;
		}

		/**
		 * Filter the decoded compatibility rules registry.
		 *
		 * Use this to replace or extend the bundled registry with a locally cached
		 * update source while keeping the runtime fail-closed.
		 *
		 * @hook powered_cache_compatibility_rules_registry
		 *
		 * @param {array}  $registry Registry payload.
		 * @param {string} $file     Registry file path.
		 *
		 * @return {array} New value.
		 *
		 * @since 4.0.0
		 */
		$registry = apply_filters( 'powered_cache_compatibility_rules_registry', $registry, $this->file );

		if ( ! $this->is_valid_registry( $registry ) ) {
			return $this->registry;
		}

		$registry = $this->merge_registry( $registry, $this->remote_registry() );

		$this->registry = $registry;

		return $this->registry;
	}

	/**
	 * Return a cached remote registry payload.
	 *
	 * Remote rules are optional and fail closed: bundled rules stay active when
	 * the endpoint is disabled, unavailable, or returns an invalid payload.
	 *
	 * @return array
	 */
	private function remote_registry() {
		/**
		 * Filter the remote compatibility rules registry URL.
		 *
		 * @hook powered_cache_compatibility_rules_remote_url
		 *
		 * @param {string} $url Remote registry URL.
		 *
		 * @return {string} New value.
		 * @since 4.0.0
		 */
		$url = (string) apply_filters( 'powered_cache_compatibility_rules_remote_url', '' );

		if ( '' === $url || ! $this->is_allowed_remote_url( $url ) || ! function_exists( 'wp_remote_get' ) || ! function_exists( 'get_site_transient' ) || ! function_exists( 'set_site_transient' ) ) {
			return array();
		}

		$cached = get_site_transient( self::REMOTE_CACHE_KEY );

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

		$response = wp_remote_get(
			$url,
			array(
				'timeout'     => 3,
				'redirection' => 2,
				'headers'     => array(
					'Accept' => 'application/json',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			set_site_transient( self::REMOTE_CACHE_KEY, array(), 6 * 3600 );
			return array();
		}

		$response_code = wp_remote_retrieve_response_code( $response );

		if ( 200 > $response_code || 300 <= $response_code ) {
			set_site_transient( self::REMOTE_CACHE_KEY, array(), 6 * 3600 );
			return array();
		}

		$body = wp_remote_retrieve_body( $response );

		if ( ! is_string( $body ) || strlen( $body ) > self::REMOTE_MAX_BYTES ) {
			set_site_transient( self::REMOTE_CACHE_KEY, array(), 6 * 3600 );
			return array();
		}

		$registry = json_decode( $body, true );

		if ( ! $this->is_valid_registry( $registry ) ) {
			set_site_transient( self::REMOTE_CACHE_KEY, array(), 6 * 3600 );
			return array();
		}

		set_site_transient( self::REMOTE_CACHE_KEY, $registry, 86400 );

		return $registry;
	}

	/**
	 * Determine whether a remote registry URL is allowed.
	 *
	 * @param string $url Remote registry URL.
	 *
	 * @return bool
	 */
	private function is_allowed_remote_url( $url ) {
		$parts = parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url

		if ( empty( $parts['scheme'] ) || empty( $parts['host'] ) || 'https' !== strtolower( $parts['scheme'] ) ) {
			return false;
		}

		/**
		 * Filter allowed remote compatibility registry hosts.
		 *
		 * @hook powered_cache_compatibility_rules_remote_hosts
		 *
		 * @param {array} $hosts Allowed hostnames.
		 *
		 * @return {array} New value.
		 * @since 4.0.0
		 */
		$hosts = apply_filters( 'powered_cache_compatibility_rules_remote_hosts', array( 'poweredcache.com', 'www.poweredcache.com' ) );
		$hosts = is_array( $hosts ) ? array_map( 'strtolower', array_filter( $hosts, 'is_string' ) ) : array();

		return in_array( strtolower( $parts['host'] ), $hosts, true );
	}

	/**
	 * Merge two valid registry payloads.
	 *
	 * @param array $base     Bundled registry.
	 * @param array $addition Remote registry.
	 *
	 * @return array
	 */
	private function merge_registry( array $base, array $addition ) {
		if ( ! $this->is_valid_registry( $addition ) ) {
			return $base;
		}

		foreach ( array( 'rules', 'conditional_rules' ) as $key ) {
			if ( ! empty( $addition[ $key ] ) && is_array( $addition[ $key ] ) ) {
				$base[ $key ] = $this->merge_recursive_distinct( isset( $base[ $key ] ) && is_array( $base[ $key ] ) ? $base[ $key ] : array(), $addition[ $key ] );
			}
		}

		if ( ! empty( $addition['settings_issues'] ) && is_array( $addition['settings_issues'] ) ) {
			$base['settings_issues'] = array_merge( isset( $base['settings_issues'] ) && is_array( $base['settings_issues'] ) ? $base['settings_issues'] : array(), $addition['settings_issues'] );
		}

		return $base;
	}

	/**
	 * Recursively merge registry arrays while appending numeric lists.
	 *
	 * @param array $base     Base array.
	 * @param array $addition Additional array.
	 *
	 * @return array
	 */
	private function merge_recursive_distinct( array $base, array $addition ) {
		foreach ( $addition as $key => $value ) {
			if ( is_int( $key ) ) {
				$base[] = $value;
				continue;
			}

			if ( isset( $base[ $key ] ) && is_array( $base[ $key ] ) && is_array( $value ) ) {
				$base[ $key ] = $this->merge_recursive_distinct( $base[ $key ], $value );
				continue;
			}

			$base[ $key ] = $value;
		}

		return $base;
	}

	/**
	 * Check whether a registry payload can be used.
	 *
	 * @param mixed $registry Registry payload.
	 *
	 * @return bool
	 */
	private function is_valid_registry( $registry ) {
		return is_array( $registry )
			&& self::FORMAT === ( isset( $registry['format'] ) ? $registry['format'] : '' )
			&& self::FORMAT_VERSION === ( isset( $registry['format_version'] ) ? $registry['format_version'] : '' )
			&& isset( $registry['rules'] )
			&& is_array( $registry['rules'] );
	}

	/**
	 * Return normalized unique rules from a registry bucket.
	 *
	 * @param array $rules Rule values.
	 *
	 * @return array
	 */
	private function normalize_rules( array $rules ) {
		$normalized = array();

		foreach ( $rules as $rule ) {
			if ( ! is_scalar( $rule ) ) {
				continue;
			}

			$rule = trim( (string) $rule );

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

			$normalized[] = $rule;
		}

		return $normalized;
	}

	/**
	 * Return conditional rules that match the current environment.
	 *
	 * @param array  $registry Registry payload.
	 * @param string $bucket   Rule bucket.
	 *
	 * @return array
	 */
	private function conditional_rules( array $registry, $bucket ) {
		if ( empty( $registry['conditional_rules'] ) || ! is_array( $registry['conditional_rules'] ) ) {
			return array();
		}

		$rules = $this->conditional_rules_for_sources( $registry['conditional_rules'], 'plugins', $this->active_plugins(), $bucket );
		$rules = $this->append_unique( $rules, $this->conditional_rules_for_sources( $registry['conditional_rules'], 'environments', $this->active_environments(), $bucket ) );

		return $rules;
	}

	/**
	 * Return conditional setting issues that match the current environment.
	 *
	 * @param array $registry Registry payload.
	 * @param array $settings Current settings.
	 *
	 * @return array
	 */
	private function conditional_setting_issues( array $registry, array $settings ) {
		if ( empty( $registry['conditional_rules'] ) || ! is_array( $registry['conditional_rules'] ) ) {
			return array();
		}

		$issues = $this->conditional_setting_issues_for_sources( $registry['conditional_rules'], 'plugins', $this->active_plugins(), $settings );
		$issues = $this->append_unique_issues( $issues, $this->conditional_setting_issues_for_sources( $registry['conditional_rules'], 'environments', $this->active_environments(), $settings ) );

		return $issues;
	}

	/**
	 * Return conditional rules for active source identifiers.
	 *
	 * @param array  $conditional_rules Conditional registry payload.
	 * @param string $group             Source group.
	 * @param array  $active_sources    Active source identifiers.
	 * @param string $bucket            Rule bucket.
	 *
	 * @return array
	 */
	private function conditional_rules_for_sources( array $conditional_rules, $group, array $active_sources, $bucket ) {
		if ( empty( $conditional_rules[ $group ] ) || ! is_array( $conditional_rules[ $group ] ) ) {
			return array();
		}

		$rules = array();

		foreach ( $conditional_rules[ $group ] as $source => $source_rules ) {
			if ( ! is_string( $source ) || ! in_array( $source, $active_sources, true ) || empty( $source_rules[ $bucket ] ) || ! is_array( $source_rules[ $bucket ] ) ) {
				continue;
			}

			$rules = $this->append_unique( $rules, $this->normalize_rules( $source_rules[ $bucket ] ) );
		}

		return $rules;
	}

	/**
	 * Return conditional setting issues for active source identifiers.
	 *
	 * @param array  $conditional_rules Conditional registry payload.
	 * @param string $group             Source group.
	 * @param array  $active_sources    Active source identifiers.
	 * @param array  $settings          Current settings.
	 *
	 * @return array
	 */
	private function conditional_setting_issues_for_sources( array $conditional_rules, $group, array $active_sources, array $settings ) {
		if ( empty( $conditional_rules[ $group ] ) || ! is_array( $conditional_rules[ $group ] ) ) {
			return array();
		}

		$issues = array();

		foreach ( $conditional_rules[ $group ] as $source => $source_rules ) {
			if ( ! is_string( $source ) || ! in_array( $source, $active_sources, true ) || empty( $source_rules['settings_issues'] ) || ! is_array( $source_rules['settings_issues'] ) ) {
				continue;
			}

			$issues = $this->append_unique_issues( $issues, $this->setting_issues_from_items( $source_rules['settings_issues'], $settings ) );
		}

		return $issues;
	}

	/**
	 * Return active plugin basenames.
	 *
	 * @return array
	 */
	private function active_plugins() {
		if ( null !== $this->active_plugins ) {
			return $this->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 ) );
			}
		}

		$plugins = $this->normalize_rules( $plugins );

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

		$this->active_plugins = is_array( $plugins ) ? $this->normalize_rules( $plugins ) : array();

		return $this->active_plugins;
	}

	/**
	 * Return active hosting/runtime environment identifiers.
	 *
	 * @return array
	 */
	private function active_environments() {
		if ( null !== $this->active_environments ) {
			return $this->active_environments;
		}

		$environments = $this->detect_environments();

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

		$this->active_environments = is_array( $environments ) ? $this->normalize_rules( $environments ) : array();

		return $this->active_environments;
	}

	/**
	 * Detect common hosting/runtime environments.
	 *
	 * @return array
	 */
	private function detect_environments() {
		$environments = array();
		$software     = $this->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 ( '' !== $this->server_value( 'HTTP_CF_RAY' ) || false !== stripos( $this->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';
		}

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

	/**
	 * Return a sanitized server value.
	 *
	 * @param string $key Server key.
	 *
	 * @return string
	 */
	private 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 );
	}

	/**
	 * Return normalized setting issues from registry items.
	 *
	 * @param array $items    Registry issue items.
	 * @param array $settings Current settings.
	 *
	 * @return array
	 */
	private function setting_issues_from_items( array $items, array $settings ) {
		$issues = array();

		foreach ( $items as $item ) {
			$issue = $this->normalize_setting_issue( $item, $settings );

			if ( empty( $issue ) ) {
				continue;
			}

			$issues[] = $issue;
		}

		return $issues;
	}

	/**
	 * Return a normalized setting issue when it matches the current settings.
	 *
	 * @param mixed $item     Registry issue item.
	 * @param array $settings Current settings.
	 *
	 * @return array
	 */
	private function normalize_setting_issue( $item, array $settings ) {
		if ( ! is_array( $item ) || ! $this->setting_issue_matches( $item, $settings ) ) {
			return array();
		}

		$key      = isset( $item['key'] ) && is_scalar( $item['key'] ) ? trim( (string) $item['key'] ) : '';
		$severity = isset( $item['severity'] ) && is_scalar( $item['severity'] ) ? trim( (string) $item['severity'] ) : '';
		$code     = isset( $item['code'] ) && is_scalar( $item['code'] ) ? trim( (string) $item['code'] ) : '';
		$message  = isset( $item['message'] ) && is_scalar( $item['message'] ) ? trim( (string) $item['message'] ) : '';

		if ( '' === $key || '' === $code || '' === $message || ! in_array( $severity, array( 'error', 'warning', 'info' ), true ) ) {
			return array();
		}

		return array(
			'key'      => $key,
			'severity' => $severity,
			'code'     => $code,
			'message'  => $message,
		);
	}

	/**
	 * Determine whether a registry issue applies to the current settings.
	 *
	 * @param array $item     Registry issue item.
	 * @param array $settings Current settings.
	 *
	 * @return bool
	 */
	private function setting_issue_matches( array $item, array $settings ) {
		if ( empty( $item['when'] ) ) {
			return true;
		}

		if ( ! is_array( $item['when'] ) ) {
			return false;
		}

		if ( empty( $item['when']['setting'] ) || ! is_scalar( $item['when']['setting'] ) ) {
			return ! empty( $item['when']['options'] ) && $this->setting_issue_options_match( $item['when'] );
		}

		$setting = (string) $item['when']['setting'];

		if ( ! array_key_exists( $setting, $settings ) ) {
			return false;
		}

		if ( array_key_exists( 'value', $item['when'] ) ) {
			if ( is_bool( $item['when']['value'] ) ) {
				return (bool) $settings[ $setting ] === $item['when']['value'] && $this->setting_issue_options_match( $item['when'] );
			}

			return $settings[ $setting ] === $item['when']['value'] && $this->setting_issue_options_match( $item['when'] );
		}

		return ! empty( $settings[ $setting ] ) && $this->setting_issue_options_match( $item['when'] );
	}

	/**
	 * Determine whether option checks for a registry issue match.
	 *
	 * @param array $condition Issue condition.
	 *
	 * @return bool
	 */
	private function setting_issue_options_match( array $condition ) {
		if ( empty( $condition['options'] ) ) {
			return true;
		}

		if ( ! is_array( $condition['options'] ) || ! function_exists( 'get_option' ) ) {
			return false;
		}

		foreach ( $condition['options'] as $option_condition ) {
			if ( ! is_array( $option_condition ) || empty( $option_condition['name'] ) || ! is_scalar( $option_condition['name'] ) ) {
				return false;
			}

			$value = get_option( (string) $option_condition['name'] );

			if ( array_key_exists( 'value', $option_condition ) && $value !== $option_condition['value'] ) {
				return false;
			}

			if ( ! array_key_exists( 'value', $option_condition ) && empty( $value ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Append values while preserving order and avoiding duplicates.
	 *
	 * @param array $current Current values.
	 * @param array $additions Additional values.
	 *
	 * @return array
	 */
	private function append_unique( array $current, array $additions ) {
		foreach ( $additions as $addition ) {
			if ( in_array( $addition, $current, true ) ) {
				continue;
			}

			$current[] = $addition;
		}

		return $current;
	}

	/**
	 * Append validation issues while preserving order and avoiding duplicates.
	 *
	 * @param array $current   Current issues.
	 * @param array $additions Additional issues.
	 *
	 * @return array
	 */
	private function append_unique_issues( array $current, array $additions ) {
		$seen = array();

		foreach ( $current as $issue ) {
			if ( isset( $issue['key'], $issue['code'] ) ) {
				$seen[ $issue['key'] . ':' . $issue['code'] ] = true;
			}
		}

		foreach ( $additions as $issue ) {
			if ( ! isset( $issue['key'], $issue['code'] ) ) {
				continue;
			}

			$id = $issue['key'] . ':' . $issue['code'];

			if ( isset( $seen[ $id ] ) ) {
				continue;
			}

			$current[]   = $issue;
			$seen[ $id ] = true;
		}

		return $current;
	}

	/**
	 * Return default registry file path.
	 *
	 * @return string
	 */
	private static function default_file() {
		$path = defined( 'POWERED_CACHE_PATH' ) ? rtrim( POWERED_CACHE_PATH, '/\\' ) . '/data/compatibility-rules.json' : '';

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

		return dirname( __DIR__, 2 ) . '/data/compatibility-rules.json';
	}
}