<?php
/**
 * Akeeba Backup Restoration Script
 *
 * @package   brs
 * @copyright Copyright (c)2025 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\BRS\Platform\Model;

defined('_AKEEBA') or die();

class Finalise extends \Akeeba\BRS\Model\Finalise
{
	/**
	 * Update the site's .htaccess file.
	 *
	 * @return  bool
	 * @since   10.0
	 */
	public function updatehtaccess(): bool
	{
		// Get the .htaccess file to replace. If there is no file to replace we have nothing to do and can return early.
		$htaccessFileName = $this->getHtaccessFilePathToChange();

		if (is_null($htaccessFileName))
		{
			return true;
		}

		// Load the .htaccess in memory. If it's not readable return early and indicate a failure.
		$contents = @file_get_contents($htaccessFileName);

		if ($contents === false)
		{
			return false;
		}

		// Explode its lines
		$lines    = explode("\n", $contents);
		$contents = '';

		/**
		 * WordPress has two different and confusing URLs in its configuration, the Home Address (homeurl) and the
		 * WordPress Address (siteurl). These are counter-intuitive names which cause a massive amount of headaches so
		 * I'll document them here.
		 *
		 * homeurl :: the URL you want your visitors to type in to get to your Homepage.
		 * siteurl ::  the location of your core WordPress files.
		 *
		 * siteurl is typically the same as homeurl UNLESS you have the "site in subdirectory but accessed through
		 * domain root" monstrosity. In this case homeurl would be https://www.example.com but siteurl would be
		 * https://www.example.com/someotherdirectory.
		 *
		 * If you are completely insane you might have homeurl https://www.example.com/something and siteurl set to
		 * https://www.example.com/something/foobar.
		 *
		 * We have to deal with all these crazy cases.
		 */

		/** @var Replacedata $replaceModel */
		/** @var Configuration $config */
		$replaceModel = $this->getContainer()->get('mvcFactory')->model('Replacedata');
		$config       = $this->getContainer()->get('mvcFactory')->model('Configuration');

		// Is this a multisite installation?
		$isMultisite = $replaceModel->isMultisite();

		// Get the URL path (relative to domain root) where the old site was installed.
		$oldHomeURL    = $config->get('oldurl');
		$oldHomeURI    = $this->getContainer()->get('uri')->instance($oldHomeURL);
		$oldHomeFolder = $oldHomeURI->getPath();
		$oldHomeFolder = trim($oldHomeFolder, '/\\');

		// Get the URL path (relative to domain root) where the new site is installed.
		$newHomeURL = $config->get('homeurl');
		$newCoreFilesURL = $config->get('siteurl');

		// If homeurl or siteurl are missing (e.g. Setup was not completed, or session was lost) we cannot
		// safely rewrite .htaccess rules. Return false to signal failure.
		if (empty($newHomeURL) || empty($newCoreFilesURL))
		{
			return false;
		}

		$newHomeURI    = $this->getContainer()->get('uri')->instance($newHomeURL);
		$newHomeFolder = $newHomeURI->getPath();
		$newHomeFolder = trim($newHomeFolder, '/\\');

		// Get the site's URL
		$newCoreFilesURI    = $this->getContainer()->get('uri')->instance($newCoreFilesURL);
		$newCoreFilesFolder = $newCoreFilesURI->getPath();
		$newCoreFilesFolder = trim($newCoreFilesFolder, '/\\');

		// Apply replacements
		$replacements = $replaceModel->getDefaultURLReplacements();
		$replaceFrom  = array_keys($replacements);
		$replaceTo    = array_values($replacements);

		if (!empty($replacements))
		{
			$lines = array_map(
				function ($line) use ($replaceFrom, $replaceTo) {
					return str_replace($replaceFrom, $replaceTo, $line);
				}, $lines
			);
		}

		// Convert the RewriteBase line
		$lines = $this->convertRewriteBase($lines, $newCoreFilesFolder);

		// Convert the core WordPress RewriteRule
		$lines = $this->changeCoreRewriteRule($lines, $newHomeURL, $newCoreFilesURL, $newCoreFilesFolder);

		/**
		 * Multisites can be either in the domain root OR a subdirectory. Converting from one to the other requires
		 * .htaccess changes of certain RewriteRule lines. Will only work if you used the core's recommended .htaccess.
		 */
		if ($isMultisite)
		{
			$isDomainsInstall = $config->get('subdomain_install', 0);
			$lines            = $this->convertMultisiteRewriteRule(
				$lines, $newHomeFolder, $oldHomeFolder, $isDomainsInstall
			);
		}

		// If the home URL changed from the site's root to a subdirectory we need to convert some .htaccess rules
		$lines = $this->convertRootToSubdirectory($lines, $oldHomeFolder, $newHomeFolder);

		// Write the new .htaccess. Indicate failure if this is not possible.
		if (!file_put_contents($htaccessFileName, implode("\n", $lines)))
		{
			return false;
		}

		// If the homeurl and siteurl don't match, copy the .htaccess file and index.php in the correct directory
		if ($newCoreFilesURL != $newHomeURL)
		{
			return $this->handleCoreFilesInSubdirectory($newCoreFilesFolder, $newHomeFolder);
		}

		return true;
	}

	/**
	 * Find the correct file to modify: .htaccess, or htaccess.bak.
	 *
	 * Depending on the restoration method (Kickstart, integrated, UNiTE, manual extraction etc) we may have either a
	 * .htaccess file, or a htaccess.bak file we need to modify. This method picks the correct one and returns its
	 * full path.
	 *
	 * @return  null|string  The path of the file, null if nothing was found
	 * @since   10.0
	 */
	protected function getHtaccessFilePathToChange(): ?string
	{
		$paths = $this->getContainer()->get('paths');

		// Let's build the stack of possible files
		$files = [
			$paths->get('root') . '/.htaccess',
			$paths->get('root') . '/htaccess.bak',
		];

		// Do I want to give more importance to .bak file first?
		if ($this->getState('bak_first', 0))
		{
			rsort($files);
		}

		$fileName = null;

		foreach ($files as $file)
		{
			// Did I find what I'm looking for?
			if (file_exists($file))
			{
				$fileName = $file;

				break;
			}
		}

		return $fileName;
	}

	/**
	 * Handle WordPress installations with core files in a different subdirectory.
	 *
	 * Some WordPress sites have their core files in a different subdirectory than the one used to access the site.
	 *
	 * For example:
	 *
	 * Home Address (homeurl)      -- typed by visitors to access your site -- https://www.example.com/foobar
	 * WordPress Address (siteurl) -- where WordPress core files are stored --
	 * https://www.example.com/foobar/wordpress_dir
	 *
	 * In these cases we are restoring into the <webRoot>/foobar/wordpress_dir folder and our .htaccess file is there
	 * as
	 * well. However, we need to copy the .htaccess in <webRoot>foobar, copy the index.php in <webRoot>foobar and
	 * modify
	 * the index.php to load stuff from the <webRoot>/foobar/wordpress_dir subdirectory.
	 *
	 * This method handles these necessary changes.
	 *
	 * @param   string  $newCoreFilesFolder  The relative path where WordPress core files are stored
	 * @param   string  $newHomeFolder       The relative path used to access the site
	 *
	 * @return  bool  False if an error occurred, e.g. an unwriteable file
	 * @since   10.0
	 */
	protected function handleCoreFilesInSubdirectory(string $newCoreFilesFolder, string $newHomeFolder): bool
	{
		if (strpos($newCoreFilesFolder, $newHomeFolder) !== 0)
		{
			// I have no clue where to put the files so I'll do nothing at all :s
			return true;
		}

		$paths = $this->getContainer()->get('paths');

		// $newHomeFolder is WITHOUT /wordpress_dir (/foobar); $path is the one WITH /wordpress_dir (/foobar/wordpress_dir)
		$newHomeFolder        = ltrim($newHomeFolder, '/\\');
		$newCoreFilesFolder   = ltrim($newCoreFilesFolder, '/\\');
		$homeFolderParts      = explode('/', $newHomeFolder);
		$coreFilesFolderParts = explode('/', $newCoreFilesFolder);

		$numHomeParts         = count($homeFolderParts);
		$coreFilesFolderParts = array_slice($coreFilesFolderParts, $numHomeParts);

		// Relative path from HOME to SITE (WP) root
		$relativeCoreFilesPath = implode('/', $coreFilesFolderParts);

		// How many directories above the root (where we are restoring) is our site's root
		$levelsUp = count($coreFilesFolderParts);

		// Determine the path where the index.php and .htaccess files will be written to
		$targetPath = $paths->get('root') . str_repeat('/..', $levelsUp);
		$targetPath = realpath($targetPath) ? realpath($targetPath) : $targetPath;

		// Copy the .htaccess and index.php files
		if (!@copy($paths->get('root') . '/.htaccess', $targetPath . '/.htaccess'))
		{
			return false;
		}

		if (!@copy($paths->get('root') . '/index.php', $targetPath . '/index.php'))
		{
			return false;
		}

		// Edit the index.php file
		$fileName     = $targetPath . '/index.php';
		$fileContents = @file($fileName);

		if (empty($fileContents))
		{
			return false;
		}

		foreach ($fileContents as $index => $line)
		{
			$line = trim($line);

			if (strstr($line, 'wp-blog-header.php') && (strpos($line, 'require') === 0))
			{
				$line = "require( dirname( __FILE__ ) . '/$relativeCoreFilesPath/wp-blog-header.php' );";
			}

			$fileContents[$index] = $line;
		}

		$fileContents = implode("\n", $fileContents);
		@file_put_contents($fileName, $fileContents);

		return true;
	}

	/**
	 * Converts the RewriteBase anywhere in the file.
	 *
	 * @param   array   $lines               The lines of the .htaccess files.
	 * @param   string  $newCoreFilesFolder  New folder of core files.
	 *
	 * @return  array  The processed lines of the .htaccess file.
	 * @since   10.0
	 */
	protected function convertRewriteBase(array $lines, string $newCoreFilesFolder): array
	{
		return array_map(
			function ($line) use ($newCoreFilesFolder) {
				// Fix naughty Windows users' doing
				$line = rtrim($line, "\r");

				// Handle the RewriteBase line
				if (strpos(trim($line), 'RewriteBase ') === 0)
				{
					$leftMostPos   = strpos($line, 'RewriteBase');
					$leftMostStuff = substr($line, 0, $leftMostPos);

					$line = "{$leftMostStuff}RewriteBase /$newCoreFilesFolder";

					// If the site is hosted on the domain's root
					if (empty($newCoreFilesFolder))
					{
						$line = "{$leftMostStuff}RewriteBase /";
					}

					return $line;
				}

				return $line;
			}, $lines
		);
	}

	/**
	 * Convert the core's catch-all RewriteRule
	 *
	 * @param   array   $lines               The .htaccess lines.
	 * @param   string  $newHomeURL          The new Home URL (URL used to access the site).
	 * @param   string  $newCoreFilesURL     The new WordPress URL (URL where core files are saved in).
	 * @param   string  $newCoreFilesFolder  The folder part of the WordPress URL.
	 *
	 * @return  array  The processed .htaccess lines.
	 * @since   10.0
	 */
	protected function changeCoreRewriteRule(array $lines, string $newHomeURL, string $newCoreFilesURL, string $newCoreFilesFolder): array
	{
		/**
		 * Handle moving from domain root to a subdirectory (see https://codex.wordpress.org/htaccess)
		 *
		 * The thing is that WordPress ships by default with a SEF URL rule that redirects all requests to
		 * /index.php. However, this is NOT right UNLESS your homeurl and siteurl differ. In all other cases this
		 * causes your site to fail loading anything but its front page because there's no index.php in the domain's
		 * web root. This has to be changed to have JUST index.php, not /index.php
		 */
		if ($newCoreFilesURL == $newHomeURL)
		{
			return array_map(
				function ($line) use ($newCoreFilesFolder) {
					if (strpos(trim($line), 'RewriteRule . /index.php') === 0)
					{
						return str_replace('/index.php', 'index.php', $line);
					}

					return $line;
				}, $lines
			);
		}

		/**
		 * Conversely, when homeurl and siteurl differ on your NEW site (but not on the old one) we might have to
		 * change RewriteRule . index.php to RewriteRule . /index.php, otherwise the site would not load correctly.
		 */
		return array_map(
			function ($line) use ($newCoreFilesFolder) {
				if (strpos(trim($line), 'RewriteRule . index.php') === 0)
				{
					return str_replace('index.php', '/index.php', $line);
				}

				return $line;
			}, $lines
		);
	}

	/**
	 * Converts the RewriteRule lines for multisites which are restored into subdirectories
	 *
	 * @param   array   $lines             The .htaccess file's lines
	 * @param   string  $newHomeFolder     The home folder of the restored site
	 * @param   string  $oldHomeFolder     The home folder of the original site
	 * @param   bool    $isDomainsInstall  Is it a subdomains installation (blog1.example.com instead of
	 *                                     example.com/blog1)?
	 *
	 * @return  array
	 * @since   10.0
	 */
	protected function convertMultisiteRewriteRule(
		array $lines, string $newHomeFolder, string $oldHomeFolder, bool $isDomainsInstall = false
	)
	{
		// Check if the new multisite is inside a subdirectory
		$newInSubDirectory = !empty($newHomeFolder);
		// Check if the old multisite was inside a subdirectory
		$oldInSubdirectory = !empty($oldHomeFolder);

		// Both old and new sites of the same kind (both in subdirectories OR both in domain root). No conversion.
		if ($newInSubDirectory === $oldInSubdirectory)
		{
			return $lines;
		}

		/**
		 * New site in subdirectory but old site in domain root. Conversion FROM root TO subdirectory.
		 *
		 * Also applies when the new multisite uses path-style blogs, no matter if it's in the domain root or a
		 * subdirectory. In this case we want to map e.g. /blog1/wp-content to /wp-content.
		 */
		$isFromRootToSubdirectory = $newInSubDirectory && !$oldInSubdirectory;

		if ($isFromRootToSubdirectory || !$isDomainsInstall)
		{
			return array_map(
				function ($line) {
					$trimLine = trim($line);

					if (strpos($trimLine, 'RewriteRule ^wp-admin$ wp-admin/') === 0)
					{
						$line = str_replace(
							'RewriteRule ^wp-admin$ wp-admin/', 'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/',
							$line
						);

						return $line;
					}

					if (strpos($trimLine, 'RewriteRule ^(wp-(content|admin|includes).*) $1') === 0)
					{
						$line = str_replace(
							'RewriteRule ^(wp-(content|admin|includes).*) $1',
							'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2', $line
						);

						return $line;
					}

					if (strpos($trimLine, 'RewriteRule ^(.*\.php)$ $1') === 0)
					{
						$line = str_replace(
							'RewriteRule ^(.*\.php)$ $1', 'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2', $line
						);

						return $line;
					}

					return $line;
				}, $lines
			);
		}

		/**
		 * Otherwise, new multisite in domain root, old multisite in subdirectory. Conversion FROM subdirectory TO root.
		 */
		return array_map(
			function ($line) {
				$trimLine = trim($line);

				if (strpos($trimLine, 'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/') === 0)
				{
					$line = str_replace(
						'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/', 'RewriteRule ^wp-admin$ wp-admin/',
						$line
					);

					return $line;
				}

				if (strpos($trimLine, 'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2') === 0)
				{
					$line = str_replace(
						'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2',
						'RewriteRule ^(wp-(content|admin|includes).*) $1', $line
					);

					return $line;
				}

				if (strpos($trimLine, 'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2') === 0)
				{
					$line = str_replace(
						'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2', 'RewriteRule ^(.*\.php)$ $1', $line
					);

					return $line;
				}

				return $line;
			}, $lines
		);
	}

	/**
	 * Converts RewriteRule and RewriteCond lines from domain root to a subdirectory installation.
	 *
	 * This method assesses the current and target site configurations and performs replacements only in the case where
	 * the site is moved from the domain root to a subdirectory. It applies necessary replacements to accommodate
	 * the subdirectory structure for various caching and administrative tools.
	 *
	 * @param   array   $lines          The lines of the .htaccess file to process.
	 * @param   string  $oldHomeFolder  The home folder of the original site (empty string for domain root).
	 * @param   string  $newHomeFolder  The home folder of the new site (empty string for domain root, or the subdirectory path).
	 *
	 * @return  array   The processed lines of the .htaccess file with adjusted RewriteRule and RewriteCond paths.
	 * @since   10.0
	 */
	protected function convertRootToSubdirectory(array $lines, string $oldHomeFolder, string $newHomeFolder): array
	{
		/**
		 * The following cases do not warrant a replacement:
		 *
		 * FROM domain root TO domain root ==> No replacement is necessary.
		 *
		 * FROM subdirectory TO domain root ==> This is handled by the default replacements on the grounds that the old
		 *                                      site's subdirectory is a unique enough string in .htaccess which lets
		 *                                      us do straightforward string replacements.
		 *
		 * FROM subdirectory TO subdirectory ==> This is the same as above, handled by the default replacements. If the
		 *                                       subdirectory is the same nothing changed and no replacements were made.
		 *
		 * Therefore the only case I need to check for is FROM domain root TO subdirectory. So what I do here is check
		 * whether this is the case. If not, quit early.
		 */
		if (!(empty($oldHomeFolder) && !empty($newHomeFolder)))
		{
			return $lines;
		}

		// The only case left is FROM root TO subdirectory. Get the new home folder in the formats we need.
		$newHomeFolder = trim($newHomeFolder, '/');

		// The replacements we need to make
		$replacements = [
			// WP Super Cache, W3 Total Cache. WATCH OUT FOR THE LEADING SPACES / TABS. THEY ARE IMPORTANT!
			"%{DOCUMENT_ROOT}/wp-content" => "%{DOCUMENT_ROOT}/{$newHomeFolder}/wp-content",
			"%{SERVER_NAME}/"             => "%{SERVER_NAME}/{$newHomeFolder}/",
			" \"/wp-content"              => " \"/{$newHomeFolder}/wp-content",
			" /wp-content"                => " /{$newHomeFolder}/wp-content",
			"\t\"/wp-content"             => "\t\"/{$newHomeFolder}/wp-content",
			"\t/wp-content"               => "\t/{$newHomeFolder}/wp-content",
			// Admin Tools Professional for WordPress
			"://%1/$1"                    => "://%1/{$newHomeFolder}/$1",
		];

		$replaceFrom = array_keys($replacements);
		$replaceTo   = array_values($replacements);

		// Replace paths in RewriteRule and RewriteCond lines
		return array_map(
			function ($line) use ($replaceFrom, $replaceTo) {
				$trimLine = trim($line);

				if (!in_array(substr($trimLine, 0, 11), ['RewriteRule', 'RewriteCond']))
				{
					return $line;
				}

				return str_replace($replaceFrom, $replaceTo, $line);
			}, $lines
		);
	}
}