<?php

namespace XF\Service\Style;

use XF\App;
use XF\Service\AbstractService;
use XF\Util\File;

use function count, in_array;

class ArchiveValidatorService extends AbstractService
{
	protected $filePath;
	protected $archiveAction;

	/**
	 * @var array|null
	 */
	protected $knownFileHashes;

	// all paths in lower case
	public const DIR_BLACKLIST = [
		'data',
		'install',
		'internal_data',
		'library',
		'src',

		'js/vendor',
		'js/xf',
		'js/xf*',
		'styles/default/xenforo',
		'styles/default/xf*',
		'styles/fonts/fa',
	];

	// all extensions in lower case
	public const EXTENSION_WHITELIST = [
		'avi', 'eot', 'gif',
		'html', 'ico', 'jpe',
		'jpeg', 'jpg', 'js',
		'json', 'less', 'map',
		'md', 'mov', 'mp4',
		'otf', 'png', 'scss',
		'svg', 'ttf', 'txt',
		'webp', 'woff', 'woff2',
		'zip',
	];

	public function __construct(App $app, string $filePath, string $archiveAction)
	{
		parent::__construct($app);

		if (!file_exists($filePath) || !is_dir($filePath))
		{
			throw new \InvalidArgumentException("Invalid file path passed in ($filePath)");
		}

		$this->filePath = $filePath;

		switch ($archiveAction)
		{
			case 'import':
			case 'export':
				break;
			default:
				throw new \InvalidArgumentException('Only archive actions of \'import\' or \'export\' are valid.');
		}
		$this->archiveAction = $archiveAction;
	}

	public function validate(&$errors = []): bool
	{
		$errors = [];

		$rootPath = $this->filePath;
		$disallowedExtFiles = [];

		$files = File::getRecursiveDirectoryIterator($rootPath);
		foreach ($files AS $file)
		{
			if ($file->isDir())
			{
				continue;
			}

			$pathname = $file->getPathname();
			$basename = $file->getBasename();

			if ($basename === '' || $basename === false || $basename === null)
			{
				continue;
			}

			if ($this->isFileIgnored($basename))
			{
				// skip this but no error
				continue;
			}

			if ($basename[0] == '.' && $basename != '.htaccess')
			{
				if (!isset($errors['dot_file']))
				{
					$errors['dot_file'] = \XF::phrase('one_or_more_files_in_style_archive_disallowed_dot_files');
				}
				continue;
			}

			$stdPath = File::stripRootPathPrefix($pathname, $rootPath);
			$stdPath = $this->standardizePathForValidation($stdPath);

			if ($this->isFileInRootDirectory($stdPath))
			{
				if (!isset($errors['root_dir']))
				{
					$errors['root_dir'] = \XF::phrase('one_or_more_files_in_style_archive_contained_within_root');
				}
				continue;
			}

			if ($this->isFileInBlacklistedDirectory($stdPath))
			{
				if (!isset($errors['blacklisted_dir']))
				{
					$errors['blacklisted_dir'] = \XF::phrase('one_or_more_files_in_style_archive_within_unsupported_directory_x', ['disallowed' => implode(', ', self::DIR_BLACKLIST)]);
				}
				continue;
			}

			$extension = $file->getExtension();
			if (!$this->isFileWithWhitelistedExtension($extension))
			{
				$disallowedExtFiles[] = $stdPath;
				continue;
			}

			if ($this->isCoreFile($stdPath))
			{
				if (!isset($errors['core_file']))
				{
					$errors['core_file'] = \XF::phrase('one_or_more_files_not_permitted_as_they_belong_to_xenforo_or_add_on');
				}
				continue;
			}
		}

		if ($disallowedExtFiles)
		{
			$errors['whitelisted_ext'] = \XF::phrase('one_or_more_files_do_not_have_allowed_extension_x', [
				'allowed' => implode(', ', self::EXTENSION_WHITELIST),
				'files' => implode(', ', $disallowedExtFiles),
			]);
		}

		return count($errors) == 0;
	}

	protected function standardizePathForValidation(string $path): string
	{
		// standardize on forward slashes for paths only
		$path = str_replace('\\', '/', $path);

		// avoid case sensitivity issues (as FS settings may vary)
		$path = strtolower($path);

		return $path;
	}

	protected function isFileIgnored(string $basename): bool
	{
		return $basename === '.DS_Store' || $basename === 'Thumbs.db';
	}

	protected function isFileInRootDirectory(string $path): bool
	{
		return strpos($path, '/') === false;
	}

	protected function isFileInBlacklistedDirectory(string $path): bool
	{
		foreach (self::DIR_BLACKLIST AS $dir)
		{
			$suffix = '/';
			if (substr($dir, -1) === '*')
			{
				$suffix = '';
				$dir = rtrim($dir, '*');
			}

			if (strpos($path, $dir . $suffix) === 0)
			{
				return true;
			}
		}

		return false;
	}

	protected function isFileWithWhitelistedExtension(string $extension): bool
	{
		return (
			$extension === ''
			|| in_array(strtolower($extension), self::EXTENSION_WHITELIST)
		);
	}

	protected function isCoreFile(string $path): bool
	{
		switch ($path)
		{
			case '.htaccess':
			case 'htaccess.txt':
				// core files that shouldn't be overwritten, as people may change them manually
				return true;
		}

		$hashes = $this->getKnownFileHashes();
		return isset($hashes[$path]);
	}

	protected function getKnownFileHashes()
	{
		if ($this->knownFileHashes === null)
		{
			$jsonPath = \XF::getAddOnDirectory() . \XF::$DS . 'XF' . \XF::$DS . 'hashes.json';
			if (file_exists($jsonPath))
			{
				$hashes = json_decode(file_get_contents($jsonPath), true);
			}
			else
			{
				$hashes = [];
			}

			$addOns = $this->app->addOnManager()->getAllAddOns();
			foreach ($addOns AS $addOn)
			{
				$path = $addOn->getHashesPath();
				if ($path && file_exists($path))
				{
					$decodedHashes = json_decode(file_get_contents($path), true) ?? [];
					$hashes += $decodedHashes;
				}
			}

			$this->knownFileHashes = $hashes;
		}

		return $this->knownFileHashes;
	}
}
