<?php
// phpcs:ignoreFile
/**
 * Rewrite file-relative URIs as root-relative in CSS files
 *
 * @package Minify
 * @author Stephen Clay <steve@mrclay.org>
 */

namespace LiteSpeed\Lib;

defined( 'WPINC' ) || exit;

class UriRewriter {


	/**
	 * rewrite() and rewriteRelative() append debugging information here
	 *
	 * @var string
	 */
	public static $debugText = '';

	/**
	 * In CSS content, rewrite file relative URIs as root relative
	 *
	 * @param string $css
	 *
	 * @param string $currentDir The directory of the current CSS file.
	 *
	 * @param string $docRoot The document root of the web site in which
	 * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']).
	 *
	 * @param array  $symlinks (default = array()) If the CSS file is stored in
	 *  a symlink-ed directory, provide an array of link paths to
	 *  target paths, where the link paths are within the document root. Because
	 *  paths need to be normalized for this to work, use "//" to substitute
	 *  the doc root in the link paths (the array keys). E.g.:
	 *  <code>
	 *  array('//symlink' => '/real/target/path') // unix
	 *  array('//static' => 'D:\\staticStorage')  // Windows
	 *  </code>
	 *
	 * @return string
	 */
	public static function rewrite( $css, $currentDir, $docRoot = null, $symlinks = array() ) {
		self::$_docRoot    = self::_realpath(
			$docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
		);
		self::$_currentDir = self::_realpath( $currentDir );
		self::$_symlinks   = array();

		// normalize symlinks in order to map to link
		foreach ( $symlinks as $link => $target ) {
			$link = ( $link === '//' ) ? self::$_docRoot : str_replace( '//', self::$_docRoot . '/', $link );
			$link = strtr( $link, '/', DIRECTORY_SEPARATOR );

			self::$_symlinks[ $link ] = self::_realpath( $target );
		}

		self::$debugText .= 'docRoot    : ' . self::$_docRoot . "\n"
							. 'currentDir : ' . self::$_currentDir . "\n";
		if ( self::$_symlinks ) {
			self::$debugText .= 'symlinks : ' . var_export( self::$_symlinks, 1 ) . "\n";
		}
		self::$debugText .= "\n";

		$css = self::_trimUrls( $css );

		$css = self::_owlifySvgPaths( $css );

		// rewrite
		$pattern = '/@import\\s+([\'"])(.*?)[\'"]/';
		$css     = preg_replace_callback( $pattern, __CLASS__ . '::_processUriCB', $css );

		$pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/';
		$css     = preg_replace_callback( $pattern, __CLASS__ . '::_processUriCB', $css );

		$css = self::_unOwlify( $css );

		return $css;
	}

	/**
	 * In CSS content, prepend a path to relative URIs
	 *
	 * @param string $css
	 *
	 * @param string $path The path to prepend.
	 *
	 * @return string
	 */
	public static function prepend( $css, $path ) {
		self::$_prependPath = $path;

		$css = self::_trimUrls( $css );

		$css = self::_owlifySvgPaths( $css );

		// append
		$pattern = '/@import\\s+([\'"])(.*?)[\'"]/';
		$css     = preg_replace_callback( $pattern, __CLASS__ . '::_processUriCB', $css );

		$pattern = '/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/';
		$css     = preg_replace_callback( $pattern, __CLASS__ . '::_processUriCB', $css );

		$css = self::_unOwlify( $css );

		self::$_prependPath = null;

		return $css;
	}

	/**
	 * Get a root relative URI from a file relative URI
	 *
	 * <code>
	 * UriRewriter::rewriteRelative(
	 *       '../img/hello.gif'
	 *     , '/home/user/www/css'  // path of CSS file
	 *     , '/home/user/www'      // doc root
	 * );
	 * // returns '/img/hello.gif'
	 *
	 * // example where static files are stored in a symlinked directory
	 * UriRewriter::rewriteRelative(
	 *       'hello.gif'
	 *     , '/var/staticFiles/theme'
	 *     , '/home/user/www'
	 *     , array('/home/user/www/static' => '/var/staticFiles')
	 * );
	 * // returns '/static/theme/hello.gif'
	 * </code>
	 *
	 * @param string $uri file relative URI
	 *
	 * @param string $realCurrentDir realpath of the current file's directory.
	 *
	 * @param string $realDocRoot realpath of the site document root.
	 *
	 * @param array  $symlinks (default = array()) If the file is stored in
	 *  a symlink-ed directory, provide an array of link paths to
	 *  real target paths, where the link paths "appear" to be within the document
	 *  root. E.g.:
	 *  <code>
	 *  array('/home/foo/www/not/real/path' => '/real/target/path') // unix
	 *  array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path')  // Windows
	 *  </code>
	 *
	 * @return string
	 */
	public static function rewriteRelative( $uri, $realCurrentDir, $realDocRoot, $symlinks = array() ) {
		// prepend path with current dir separator (OS-independent)
		$path  = strtr( $realCurrentDir, '/', DIRECTORY_SEPARATOR );
		$path .= DIRECTORY_SEPARATOR . strtr( $uri, '/', DIRECTORY_SEPARATOR );

		self::$debugText .= "file-relative URI  : {$uri}\n"
							. "path prepended     : {$path}\n";

		// "unresolve" a symlink back to doc root
		foreach ( $symlinks as $link => $target ) {
			if ( 0 === strpos( $path, $target ) ) {
				// replace $target with $link
				$path = $link . substr( $path, strlen( $target ) );

				self::$debugText .= "symlink unresolved : {$path}\n";

				break;
			}
		}
		// strip doc root
		$path = substr( $path, strlen( $realDocRoot ) );

		self::$debugText .= "docroot stripped   : {$path}\n";

		// fix to root-relative URI
		$uri = strtr( $path, '/\\', '//' );
		$uri = self::removeDots( $uri );

		self::$debugText .= "traversals removed : {$uri}\n\n";

		return $uri;
	}

	/**
	 * Remove instances of "./" and "../" where possible from a root-relative URI
	 *
	 * @param string $uri
	 *
	 * @return string
	 */
	public static function removeDots( $uri ) {
		$uri = str_replace( '/./', '/', $uri );
		// inspired by patch from Oleg Cherniy
		do {
			$uri = preg_replace( '@/[^/]+/\\.\\./@', '/', $uri, 1, $changed );
		} while ( $changed );

		return $uri;
	}

	/**
	 * Get realpath with any trailing slash removed. If realpath() fails,
	 * just remove the trailing slash.
	 *
	 * @param string $path
	 *
	 * @return mixed path with no trailing slash
	 */
	protected static function _realpath( $path ) {
		$realPath = realpath( $path );
		if ( $realPath !== false ) {
			$path = $realPath;
		}

		return rtrim( $path, '/\\' );
	}

	/**
	 * Directory of this stylesheet
	 *
	 * @var string
	 */
	private static $_currentDir = '';

	/**
	 * DOC_ROOT
	 *
	 * @var string
	 */
	private static $_docRoot = '';

	/**
	 * directory replacements to map symlink targets back to their
	 * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
	 *
	 * @var array
	 */
	private static $_symlinks = array();

	/**
	 * Path to prepend
	 *
	 * @var string
	 */
	private static $_prependPath = null;

	/**
	 * @param string $css
	 *
	 * @return string
	 */
	private static function _trimUrls( $css ) {
		$pattern = '/
            url\\(      # url(
            \\s*
            ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
            \\s*
            \\)         # )
        /x';

		return preg_replace( $pattern, 'url($1)', $css );
	}

	/**
	 * @param array $m
	 *
	 * @return string
	 */
	private static function _processUriCB( $m ) {
		// $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
		$isImport = ( $m[0][0] === '@' );
		// determine URI and the quote character (if any)
		if ( $isImport ) {
			$quoteChar = $m[1];
			$uri       = $m[2];
		} else {
			// $m[1] is either quoted or not
			$quoteChar = ( $m[1][0] === "'" || $m[1][0] === '"' ) ? $m[1][0] : '';

			$uri = ( $quoteChar === '' ) ? $m[1] : substr( $m[1], 1, strlen( $m[1] ) - 2 );
		}

		if ( $uri === '' ) {
			return $m[0];
		}

		// if not anchor id, not root/scheme relative, and not starts with scheme
		if ( ! preg_match( '~^(#|/|[a-z]+\:)~', $uri ) ) {
			// URI is file-relative: rewrite depending on options
			if ( self::$_prependPath === null ) {
				$uri = self::rewriteRelative( $uri, self::$_currentDir, self::$_docRoot, self::$_symlinks );
			} else {
				$uri = self::$_prependPath . $uri;
				if ( $uri[0] === '/' ) {
					$root         = '';
					$rootRelative = $uri;
					$uri          = $root . self::removeDots( $rootRelative );
				} elseif ( preg_match( '@^((https?\:)?//([^/]+))/@', $uri, $m ) && ( false !== strpos( $m[3], '.' ) ) ) {
					$root         = $m[1];
					$rootRelative = substr( $uri, strlen( $root ) );
					$uri          = $root . self::removeDots( $rootRelative );
				}
			}
		}

		if ( $isImport ) {
			return "@import {$quoteChar}{$uri}{$quoteChar}";
		} else {
			return "url({$quoteChar}{$uri}{$quoteChar})";
		}
	}

	/**
	 * Mungs some inline SVG URL declarations so they won't be touched
	 *
	 * @link https://github.com/mrclay/minify/issues/517
	 * @see _unOwlify
	 *
	 * @param string $css
	 * @return string
	 */
	private static function _owlifySvgPaths( $css ) {
		$pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)url(\(\s*#\w+\s*\))~';

		return preg_replace( $pattern, '$1owl$2', $css );
	}

	/**
	 * Undo work of _owlify
	 *
	 * @see _owlifySvgPaths
	 *
	 * @param string $css
	 * @return string
	 */
	private static function _unOwlify( $css ) {
		$pattern = '~\b((?:clip-path|mask|-webkit-mask)\s*\:\s*)owl~';

		return preg_replace( $pattern, '$1url', $css );
	}
}
