<?php
/**
 * CDN functionalities
 *
 * @package PoweredCache
 */

namespace PoweredCache;

use \DOMDocument as DOMDocument;
use function PoweredCache\Utils\cdn_addresses;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class CDN
 */
class CDN {

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

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

		return $instance;
	}

	/**
	 * Setup hooks
	 *
	 * @since 1.0
	 */
	public function setup() {
		$settings = \PoweredCache\Utils\get_settings();

		if ( ! $settings['enable_cdn'] ) {
			return;
		}

		add_action( 'plugins_loaded', [ $this, 'cdn_setup' ] );

	}

	/**
	 * Setup CDN
	 */
	public function cdn_setup() {
		/**
		 * Filters CDN integration
		 *
		 * @hook   powered_cache_cdn_disable
		 *
		 * @param  {boolean} False by default.
		 *
		 * @return {boolean} New value.
		 * @since  2.1
		 */
		$disable_cdn = apply_filters( 'powered_cache_cdn_disable', false );

		if ( $disable_cdn ) {
			return;
		}

		add_action( 'setup_theme', [ $this, 'start_buffer' ] );
		add_filter( 'powered_cache_fo_optimized_url', array( $this, 'cdn_optimizer_url' ), 9999, 2 );

		/**
		 * Fires after setup CDN hooks.
		 *
		 * @hook  powered_cache_cdn_setup
		 *
		 * @since 1.0
		 */
		do_action( 'powered_cache_cdn_setup' );
	}

	/**
	 * Start output buffering
	 *
	 * @since 2.2
	 */
	public function start_buffer() {
		ob_start( [ '\PoweredCache\CDN', 'end_buffering' ] );
	}

	/**
	 * Replace origin URLs with CDN.
	 *
	 * @param string $contents Output buffer.
	 * @param int    $phase    Bitmask of PHP_OUTPUT_HANDLER_* constants.
	 *
	 * @return string|string[]|null
	 * @since 2.2
	 */
	private static function end_buffering( $contents, $phase ) {
		if ( $phase & PHP_OUTPUT_HANDLER_FINAL || $phase & PHP_OUTPUT_HANDLER_END ) {

			if ( ! self::skip_cdn_integration() ) {
				$rewritten_contents = self::rewriter( $contents );

				return $rewritten_contents;
			}
		}

		return $contents;
	}

	/**
	 * Whether integrate or not integrate CDN.
	 *
	 * @return bool
	 * @since 2.2
	 */
	private static function skip_cdn_integration() {
		// check request method
		if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
			return true;
		}

		// Skip CDN integration for Block Editor requests, particularly when using the media endpoint for in-editor image resizing/manipulation.
		if ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) && wp_is_json_request() ) {
			return true;
		}

		// check conditional tags
		if ( is_admin() || is_trackback() || is_robots() || is_preview() ) {
			return true;
		}

		return false;
	}

	/**
	 * Rewrite contents
	 *
	 * @param string $contents HTML Output.
	 *
	 * @return string|string[]|null
	 * @since 2.2
	 */
	public static function rewriter( $contents ) {
		// check rewrite requirements
		if ( ! is_string( $contents ) || empty( self::get_file_extensions() ) ) {
			return $contents;
		}

		$included_file_extensions_regex = quotemeta( implode( '|', self::get_file_extensions() ) );
		$urls_regex                     = '#(?:(?:[\"\'\s=>,;]|url\()\K|^)[^\"\'\s(=>,;]+(' . $included_file_extensions_regex . ')(\?[^\/?\\\"\'\s)>,]+)?(?:(?=\/?[?\\\"\'\s)>,&])|$)#i';
		$rewritten_contents             = preg_replace_callback( $urls_regex, [ '\PoweredCache\CDN', 'rewrite_url' ], $contents );

		return $rewritten_contents;
	}

	/**
	 * Rewrite the matched url.
	 *
	 * @param array $matches Matched part of content.
	 *
	 * @return mixed|string
	 * @since 2.2
	 */
	private static function rewrite_url( $matches ) {
		$file_url      = $matches[0];
		$site_hostname = ( ! empty( $_SERVER['HTTP_HOST'] ) ) ? $_SERVER['HTTP_HOST'] : wp_parse_url( home_url(), PHP_URL_HOST ); // phpcs:ignore

		/**
		 * Filters site hostname(s).
		 *
		 * @hook   powered_cache_cdn_site_hostnames
		 *
		 * @param  {array} Site hostnames for CDN replacement.
		 *
		 * @return {array} New value.
		 *
		 * @since  2.2
		 */
		$site_hostnames = (array) apply_filters( 'powered_cache_cdn_site_hostnames', array( $site_hostname ) );

		$zone         = self::get_zone_by_ext( $matches[1] );
		$cdn_hostname = self::get_best_possible_cdn_host( $zone );
		if ( empty( $cdn_hostname ) ) {
			return $file_url;
		}

		// if excluded or already using CDN hostname
		if ( self::is_excluded( $file_url ) || false !== stripos( $file_url, $cdn_hostname ) ) {
			return $file_url;
		}

		// rewrite full URL (e.g. https://www.example.com/wp..., https:\/\/www.example.com\/wp..., or //www.example.com/wp...)
		foreach ( $site_hostnames as $site_hostname ) {
			if ( stripos( $file_url, '//' . $site_hostname ) !== false || stripos( $file_url, '\/\/' . $site_hostname ) !== false ) {
				return substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) );
			}
		}

		/**
		 * Filters whether relative urls needs to rewritten or not
		 *
		 * @hook   powered_cache_cdn_rewrite_relative_urls
		 *
		 * @param  {boolean} true to automatic update.
		 *
		 * @return {boolean} New value.
		 *
		 * @since  2.2
		 */
		if ( apply_filters( 'powered_cache_cdn_rewrite_relative_urls', true ) ) { // rewrite relative URLs hook
			// rewrite relative URL (e.g. /wp-content/uploads/example.jpg)
			if ( strpos( $file_url, '//' ) !== 0 && strpos( $file_url, '/' ) === 0 ) {
				return '//' . $cdn_hostname . $file_url;
			}

			// rewrite escaped relative URL (e.g. \/wp-content\/uploads\/example.jpg)
			if ( strpos( $file_url, '\/\/' ) !== 0 && strpos( $file_url, '\/' ) === 0 ) {
				return '\/\/' . $cdn_hostname . $file_url;
			}
		}

		return $file_url;
	}

	/**
	 * Check whether given url excluded or not.
	 *
	 * @param string $file_url File URL.
	 *
	 * @return bool
	 * @since 2.2
	 */
	private static function is_excluded( $file_url ) {
		$settings = \PoweredCache\Utils\get_settings();

		// rejected file
		if ( ! empty( $settings['cdn_rejected_files'] ) ) {
			$cdn_rejected_files = preg_split( '#(\r\n|\r|\n)#', $settings['cdn_rejected_files'], - 1, PREG_SPLIT_NO_EMPTY );
			$cdn_rejected_files = implode( '|', $cdn_rejected_files );

			if ( preg_match( '#(' . $cdn_rejected_files . ')#', $file_url ) ) {
				return true;
			}
		}

		// don't replace for base64 encoded images
		if ( false !== strpos( $file_url, 'data:image' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * CDN hostname replacement for optimized URLs
	 *
	 * @param string $optimized_url Optimized assets URL
	 * @param string $path          rel path of the files
	 *
	 * @return mixed
	 */
	public function cdn_optimizer_url( $optimized_url, $path ) {
		if ( '-' === $path[0] ) {
			$path = @gzuncompress( base64_decode( substr( $path, 1 ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		}

		$zone = 'all';
		if ( $path && false !== strpos( $path, '.css' ) ) {
			$zone = 'css';
		} elseif ( $path && false !== strpos( $path, '.js' ) ) {
			$zone = 'js';
		}

		$cdn_hostname = self::get_best_possible_cdn_host( $zone );

		if ( empty( $cdn_hostname ) ) {
			return $optimized_url;
		}

		$cdn_url = '//' . $cdn_hostname;

		$optimized_url = str_replace( home_url(), $cdn_url, $optimized_url );

		return $optimized_url;
	}


	/**
	 * try to catch best cdn address to given zone
	 *
	 * @param string $zone keys of Powered_Cache_Admin_Helper::cdn_zones
	 *
	 * @return mixed  string | false
	 */
	public static function get_best_possible_cdn_host( $zone = 'all' ) {
		global $powered_cache_cdn_addresses;

		if ( ! isset( $powered_cache_cdn_addresses ) ) {
			$powered_cache_cdn_addresses = cdn_addresses();
		}

		if ( isset( $powered_cache_cdn_addresses[ $zone ] ) && is_array( $powered_cache_cdn_addresses[ $zone ] ) ) {
			// if we have multiple host for the same resource get randomly
			$random_key = array_rand( $powered_cache_cdn_addresses[ $zone ] );

			return $powered_cache_cdn_addresses[ $zone ][ $random_key ];
		}

		// fallback to primary host
		if ( isset( $powered_cache_cdn_addresses['all'] ) && is_array( $powered_cache_cdn_addresses['all'] ) ) {
			$random_key = array_rand( $powered_cache_cdn_addresses['all'] );

			return $powered_cache_cdn_addresses['all'][ $random_key ];
		}

		return false;
	}

	/**
	 * Get CDN zone by given extension.
	 *
	 * @param string $ext File extensions. Eg: .jpg, .gif, .mp3...
	 *
	 * @return string
	 * @since 2.2
	 */
	private static function get_zone_by_ext( $ext ) {
		$zone = 'all';

		/* documented in get_file_extensions */
		$image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );
		$image_extensions = array_map(
			function ( $ext ) {
				return '.' . $ext;
			},
			$image_extensions
		);

		if ( in_array( $ext, $image_extensions, true ) ) {
			$zone = 'image';
		} elseif ( '.css' === $ext ) {
			$zone = 'css';
		} elseif ( '.js' === $ext ) {
			$zone = 'js';
		}

		return $zone;
	}

	/**
	 * Get the list of supported file extensions for CDN integration.
	 *
	 * @return array|mixed|void
	 * @since 2.2
	 */
	public static function get_file_extensions() {
		/**
		 * Filters supported image extensions.
		 *
		 * @hook       powered_cache_cdn_image_extensions
		 *
		 * @param      {array} $image_extensions Supported image extensions.
		 *
		 * @return     {array} New value.
		 * @deprecated since 2.2. Use powered_cache_cdn_extensions instead.
		 * @since      1.0
		 */
		$image_extensions = apply_filters( 'powered_cache_cdn_image_extensions', array( 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'ico', 'webp', 'avif', 'svg' ) );

		$extensions = [ 'css', 'js', 'pdf', 'mp3', 'mp4', 'woff2', 'woff', 'ttf', 'otf' ];

		$file_extensions = array_map(
			function ( $ext ) {
				return '.' . $ext;
			},
			array_merge( $image_extensions, $extensions )
		);

		/**
		 * Filters supported file extensions.
		 *
		 * @hook       powered_cache_cdn_extensions
		 *
		 * @param      {array} $file_extensions Supported file extensions.
		 *
		 * @return     {array} New value.
		 * @since      2.2
		 */
		$file_extensions = apply_filters( 'powered_cache_cdn_extensions', $file_extensions );

		return $file_extensions;
	}

}

