<?php

if (!defined('WPO_VERSION')) die('No direct access allowed');

class WP_Optimize_Htaccess {

	/**
	 * Full path to .htaccess file.
	 *
	 * @var string
	 */
	private $_htaccess_file = '';

	/**
	 * Structured content of .htaccess file.
	 *
	 * @var array
	 */
	private	$_file_tree = array();

	/**
	 * WP_Optimize_Htaccess constructor.
	 *
	 * @param string $htaccess_file Full path to .htaccess file.
	 */
	public function __construct($htaccess_file = '') {
		$this->_htaccess_file = ('' != $htaccess_file) ? $htaccess_file : $this->get_home_path() . '.htaccess';
		// read .htaccess content into $_file_tree.
		$this->read_file();
	}

	/**
	 * Returns .htaccess filename.
	 *
	 * @return string
	 */
	public function get_filename() {
		return $this->_htaccess_file;
	}

	/**
	 * Checks if .htaccess file exists.
	 *
	 * @return bool
	 */
	public function is_exists() {
		return is_file($this->_htaccess_file);
	}

	/**
	 * Checks if .htaccess file is readaable.
	 *
	 * @return bool
	 */
	public function is_readable() {
		return is_readable($this->_htaccess_file);
	}

	/**
	 * Checks if .htaccess file is writable.
	 *
	 * @return bool
	 */
	public function is_writable() {
		return is_writable($this->_htaccess_file);
	}

	/**
	 * Read content of .htaccess file and store it as a tree in $_file_tree variable.
	 * For ex.
	 *	[0] => '# BEGIN WordPress',
	 *	[1] => [
	 * 		[0] => '<IfModule mod_rewrite.c>',
	 *		[1] => 'RewriteEngine On',
	 *		[2] => 'RewriteBase /',
	 *		[3] => 'RewriteRule ^index\.php$ - [L]',
	 *		[4] => 'RewriteCond %{REQUEST_FILENAME} !-f',
	 *		[5] => 'RewriteCond %{REQUEST_FILENAME} !-d',
	 *		[6] => 'RewriteRule . /index.php [L]',
	 *		[7] => '</IfModule>',
	 *	[2] => '# END WordPress'
	 *
	 * @return void
	 */
	public function read_file() {
		if (false == $this->is_exists() || false == $this->is_readable()) return;

		$content = file_get_contents($this->_htaccess_file);

		$content = explode("\n", $content);

		$content_tree = array();

		$section = array();
		$sections = array();

		foreach ($content as $line) {
			$line = trim($line);

			if (preg_match("/^\<\/(.+)(\s.*)?\>/", $line, $matches)) {
				$section[] = $line;

				// close section
				if (!empty($sections)) {
					$_section = $section;
					$section = array_pop($sections);
					$section[] = $_section;
				} else {
					$content_tree[] = $section;
					$section = array();
				}
			} elseif (preg_match('/^\<(.+)>/', $line, $matches)) {
				// open section
				if (!empty($section)) {
					$sections[] = $section;
				}

				$section = array();
				$section[] = $line;
			} elseif (!empty($section)) {
				$section[] = $line;
			} else {
				$content_tree[] = $line;
			}
		}

		$this->_file_tree = $content_tree;
	}

	/**
	 * Write current $_file_tree content into .htaccess file.
	 */
	public function write_file() {
		$content = implode(PHP_EOL, $this->get_flat_array($this->_file_tree));
		if ($this->is_writable()) {
			@file_put_contents($this->_htaccess_file, $content); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Hosts like WP Engine restricts use of `.htaccess` file
		}
	}

	/**
	 * Recursive function used to prepare data for output - build flat array from $_file_tree.
	 *
	 * @param array  $array
	 * @param string $prefix
	 *
	 * @return array
	 */
	public function get_flat_array($array, $prefix = '') {
		$flat_array = array();

		if (!empty($array)) {
			foreach ($array as $item) {
				if (is_array($item)) {
					$item = $this->get_flat_array($item, "\t");
					$flat_array = array_merge($flat_array, $item);
				} else {
					$flat_array[] = $item;
				}
			}
		}

		reset($flat_array);
		$first = key($flat_array);
		end($flat_array);
		$last = key($flat_array);

		foreach ($flat_array as $key => $value) {
			if ('' != $value && '#' == $value[0]) {
				// never add prefix for comment lines.
				$flat_array[$key] = $value;
			} else {
				$flat_array[$key] = ($key == $first || $key == $last) ? $value : $prefix . $value;
			}
		}

		return $flat_array;
	}

	/**
	 * Update commented section in array $_file_tree, i.e. section wrapped with comments
	 * # BEGIN WP-Optimize Browser Cache
	 * ...
	 * # END WP-Optimize Browser Cache
	 *
	 * @param array  $content
	 * @param string $section
	 */
	public function update_commented_section($content, $section = 'WP-Optimize Browser Cache', $prepend = false) {
		$section_begin = $this->get_section_begin_comment($section);
		$section_end = $this->get_section_end_comment($section);

		// add begin-end section comments.
		array_unshift($content, $section_begin);
		array_push($content, $section_end);

		$section_index = $this->search_commented_section($section);

		// check if section with cache settings already in the file.
		if (false === $section_index) {
			// no section in file then add it to the end of file.
			$this->_file_tree = $prepend ? array_merge($content, $this->_file_tree) : array_merge($this->_file_tree, $content);
		} else {
			$remove_length = (false === $section_index['end']) ? null : ($section_index['end'] - $section_index['begin'] + 1);
			array_splice($this->_file_tree, $section_index['begin'], $remove_length, $content);
		}
	}

	/**
	 * Removes commented section in $_file_tree, i.e. section wrapped with comments
	 * # BEGIN WP-Optimize Browser Cache
	 *   ...
	 * # END WP-Optimize Browser Cache
	 *
	 * @param string $comment
	 *
	 * @return bool
	 */
	public function remove_commented_section($comment = 'WP-Optimize Browser Cache') {
		$section_index = $this->search_commented_section($comment);
		if (false === $section_index) return false;

		$remove_length = (false === $section_index['end']) ? null : ($section_index['end'] - $section_index['begin'] + 1);
		array_splice($this->_file_tree, $section_index['begin'], $remove_length);

		$this->_file_tree = array_values($this->_file_tree);

		return true;
	}

	/**
	 * Check if section exists wrapped by comments like
	 *
	 * # BEGIN WP-Optimize Browser Cache
	 * ...
	 * # END WP-Optimize Browser Cache
	 *
	 * @param string $section
	 * @return bool
	 */
	public function is_commented_section_exists($section = 'WP-Optimize Browser Cache') {
		$search = $this->search_commented_section($section);

		return (false === $search) ? false : true;
	}

	/**
	 * Search section in $_file_tree array wrapped by begin and end comments.
	 *
	 * @param string $section
	 * @return array|bool
	 */
	private function search_commented_section($section) {
		$section_begin = $this->get_section_begin_comment($section);
		$section_end = $this->get_section_end_comment($section);

		$section_begin_index = $section_end_index = false;

		$section_begin_normalized = $this->normalize_string($section_begin);
		$section_end_normalized = $this->normalize_string($section_end);

		foreach ($this->_file_tree as $i => $value) {
			// if it is subsection then we don't go in deep.
			if (is_array($value)) continue;

			$value = $this->normalize_string($value);

			if ($value == $section_begin_normalized) $section_begin_index = $i;
			if ($value == $section_end_normalized) $section_end_index = $i;
		}

		if (false === $section_begin_index) {
			return false;
		} else {
			return array(
				'begin' => $section_begin_index,
				'end' => $section_end_index,
			);
		}
	}

	/**
	 * Generate begin cache section comment.
	 *
	 * @param string $section
	 * @return string
	 */
	public function get_section_begin_comment($section = 'WP-Optimize Browser Cache') {
		return '# BEGIN ' . $section;
	}

	/**
	 * Generate end cache section comment.
	 *
	 * @param string $section
	 * @return string
	 */
	public function get_section_end_comment($section = 'WP-Optimize Browser Cache') {
		return '# END ' . $section;
	}

	/**
	 * Normalize string - make all letters lowercase and remove spaces.
	 *
	 * @param string $string
	 * @return string
	 */
	private function normalize_string($string) {
		return strtolower(str_replace(array("\n", "\r", ' '), '', $string));
	}

	/**
	 * Get the absolute filesystem path to the root of the WordPress installation.
	 * WP_Core function from wp-admin/includes/file.php.
	 *
	 * @since 1.5.0
	 *
	 * @return string Full filesystem path to the root of the WordPress installation
	 */
	private function get_home_path() {
		if (function_exists('get_home_path')) {
			return get_home_path();
		}

		$home    = set_url_scheme(get_option('home'), 'http');
		$siteurl = set_url_scheme(get_option('siteurl'), 'http');
		if (!empty($home) && 0 !== strcasecmp($home, $siteurl)) {
			$wp_path_rel_to_home = str_ireplace($home, '', $siteurl); /* $siteurl - $home */
			$pos                 = strripos(str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME']), trailingslashit($wp_path_rel_to_home));
			$home_path           = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos);
			$home_path           = trailingslashit($home_path);
		} else {
			$home_path = ABSPATH;
		}

		return str_replace('\\', '/', $home_path);
	}
}
