<?php
/**
 * @package   awf
 * @copyright Copyright (c)2014-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU GPL version 3 or later
 */

namespace Awf\Filesystem;

use Awf\Application\Application;
use Awf\Container\Container;
use Awf\Container\ContainerAwareInterface;
use Awf\Container\ContainerAwareTrait;
use Awf\Exception\App;

/**
 * FTP filesystem abstraction layer
 */
class Ftp implements FilesystemInterface, ContainerAwareInterface
{
	use ContainerAwareTrait;

	/**
	 * FTP server's hostname or IP address
	 *
	 * @var  string
	 */
	private $host = 'localhost';

	/**
	 * FTP server's port, default: 21
	 *
	 * @var  integer
	 */
	private $port = 21;

	/**
	 * Username used to authenticate to the FTP server
	 *
	 * @var  string
	 */
	private $username = '';

	/**
	 * Password used to authenticate to the FTP server
	 *
	 * @var  string
	 */
	private $password = '';

	/**
	 * FTP initial directory
	 *
	 * @var  string
	 */
	private $directory = '/';

	/**
	 * Should I use SSL to connect to the server (FTP over explicit SSL, a.k.a. FTPS)?
	 *
	 * @var  boolean
	 */
	private $ssl = false;

	/**
	 * Should I use FTP passive mode?
	 *
	 * @var bool
	 */
	private $passive = true;

	/**
	 * The FTP connection handle
	 *
	 * @var  resource|null
	 */
	private $connection = null;

	/**
	 * Public constructor
	 *
	 * @param   array           $options    Configuration options for the filesystem abstraction object
	 * @param   Container|null  $container  Application container
	 *
	 * @throws  App
	 */
	public function __construct(array $options, ?Container $container = null)
	{
		/** @deprecated 2.0 The container argument will become mandatory */
		if (empty($container))
		{
			trigger_error(
				sprintf('The container argument is mandatory in %s', __METHOD__),
				E_USER_DEPRECATED
			);

			$container = Application::getInstance()->getContainer();
		}

        $this->setContainer($container);

		if (isset($options['host']))
		{
			$this->host = $options['host'];
		}

		if (isset($options['port']))
		{
			$this->port = (int)$options['port'];
		}

		if (isset($options['username']))
		{
			$this->username = $options['username'];
		}

		if (isset($options['password']))
		{
			$this->password = $options['password'];
		}

		if (isset($options['directory']))
		{
			$this->directory = '/' . ltrim(trim($options['directory']), '/');
		}

		if (isset($options['ssl']))
		{
			$this->ssl = $options['ssl'];
		}

		if (isset($options['passive']))
		{
			$this->passive = $options['passive'];
		}

		$this->connect();
	}

	/**
	 * Connect to the FTP server
	 *
	 * @throws  \RuntimeException
	 */
	public function connect()
	{
		// Try to connect to the server
		if($this->ssl)
		{
			if(function_exists('ftp_ssl_connect'))
			{
				$this->connection = @ftp_ssl_connect($this->host, $this->port);
			}
			else
			{
				$this->connection = false;
				throw new \RuntimeException('ftp_ssl_connect not available on this server', 500);
			}
		}
		else
		{
			$this->connection = @ftp_connect($this->host, $this->port);
		}

		if ($this->connection === false)
		{
			throw new \RuntimeException(sprintf('Cannot connect to FTP server [host:port] = %s:%s', $this->host, $this->port), 500);
		}

		// Attempt to authenticate
		if (!@ftp_login($this->connection, $this->username, $this->password))
		{
			@ftp_close($this->connection);
			$this->connection = null;

			throw new \RuntimeException(sprintf('Cannot log in to FTP server [username:password] = %s:%s', $this->username, $this->password), 500);
		}

		// Attempt to change to the initial directory
		if (!@ftp_chdir($this->connection, $this->directory))
		{
			@ftp_close($this->connection);
			$this->connection = null;

			throw new \RuntimeException(sprintf('Cannot change to initial FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $this->directory), 500);
		}

		// Apply the passive mode preference
		@ftp_pasv($this->connection, $this->passive);
	}

	/**
	 * Public destructor, closes any open FTP connections
	 */
	public function __destruct()
	{
		if (!is_null($this->connection))
		{
			@ftp_close($this->connection);
		}
	}

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents)
	{
		$targetFile = $this->translatePath($fileName);

		// Make sure the awf:// wrapper is loaded
		class_exists('\\Awf\\Utils\\Buffer', true);

		$handle = fopen('awf://awf_filesystem_ftp', 'r+');
		fwrite($handle, $contents);
		rewind($handle);

		$ret = @ftp_fput($this->connection, $targetFile, $handle, FTP_BINARY);

		fclose($handle);

		return $ret;
	}

	/**
	 * Delete a file (remove it from the disk)
	 *
	 * @param   string  $fileName  The full path to the file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName)
	{
		$targetFile = $this->translatePath($fileName);

		return @ftp_delete($this->connection, $targetFile);
	}

	/**
	 * Create a copy of the file. Actually, we have to read it in memory and upload it again.
	 *
	 * @param   string  $from  The full path of the file to copy from
	 * @param   string  $to    The full path of the file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to)
	{
		$fromFile = $this->translatePath($from);
		$toFile   = $this->translatePath($to);

		// Make sure the awf:// wrapper is loaded
		class_exists('\\Awf\\Utils\\Buffer', true);

		$handle = fopen('awf://awf_filesystem_ftp', 'r+');

		$ret = @ftp_fget($this->connection, $handle, $fromFile, FTP_BINARY);

		if ($ret !== false)
		{
			rewind($handle);
			$ret = @ftp_fput($this->connection, $toFile, $handle, FTP_BINARY);
		}

		fclose($handle);

		return $ret;
	}

	/**
	 * Move or rename a file
	 *
	 * @param   string  $from  The full path of the file to move
	 * @param   string  $to    The full path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to)
	{
		$fromFile = $this->translatePath($from);
		$toFile = $this->translatePath($to);

		return @ftp_rename($this->connection, $fromFile, $toFile);
	}

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions)
	{
		$targetFile = $this->translatePath($fileName);

		return (@ftp_chmod($this->connection, $permissions, $targetFile) !== false);
	}

    /**
     * Return the current working dir
     *
     * @return  string
     */
    public function cwd()
    {
        return @ftp_pwd($this->connection);
    }

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755)
	{
		$targetDir = $this->translatePath($dirName);
		$targetDir = trim($targetDir, '/');

		$initialDir = str_replace('\\', '/', $this->directory);
		$initialDir = trim($initialDir, '/');

		// Of course I can't create the directory the application is located in :)
		if ($initialDir == $targetDir)
		{
			return true;
		}

		$directories = explode('/', $targetDir);
		$siteRootDir = $this->container['filesystemBase'];

		$localDir  = rtrim($siteRootDir, '/');
		$remoteDir = '/' . $initialDir;

		foreach ($directories as $dir)
		{
			$localDir  .= '/' . $dir;
			$remoteDir .= '/' . $dir;

			if (!is_dir($localDir))
			{
				$ret = @ftp_mkdir($this->connection, $remoteDir);

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

		$this->chmod($dirName, $permissions);

		return true;
	}

	/**
	 * Remove a directory if it exists.
	 *
	 * @param   string   $dirName    The full path of the directory to remove
	 * @param   boolean  $recursive  Should I remove its contents recursively? Otherwise it will fail if the directory
	 *                               is not empty.
	 *
	 * @return mixed
	 */
	public function rmdir($dirName, $recursive = true)
	{
		if (!$recursive)
		{
			$targetDir = $this->translatePath($dirName);

			return @ftp_rmdir($this->connection, $targetDir);
		}
		else
		{
			if (!is_dir($dirName))
			{
				return $this->delete($dirName);
			}

			$ret = true;
			$di  = new \DirectoryIterator($dirName);

			/** @var \DirectoryIterator $dirEntry */
			foreach ($di as $dirEntry)
			{
				if ($dirEntry->isDot())
				{
					continue;
				}

				if ($dirEntry->isFile())
				{
					$ret = $ret && $this->delete($dirEntry->getPathname());
				}
				elseif ($dirEntry->isDir())
				{
					$ret = $ret && $this->rmdir($dirEntry->getPathname(), true);
				}
			}

			$ret = $ret && $this->rmdir($dirName, false);

			return $ret;
		}
	}

	/**
	 * Translate an absolute filesystem path into a relative FTP path
	 *
	 * @param   string  $fileName  The full filesystem path of a file or directory
	 *
	 * @return  string  The translated path for use by the filesystem abstraction
	 */
	public function translatePath($fileName)
	{
		$fileName = str_replace('\\', '/', $fileName);

		$siteRootDir = $this->container['filesystemBase'];

		$appRoot = str_replace('\\', '/', $siteRootDir);
		$appRoot = rtrim($appRoot, '/');

		if (strpos($fileName, $appRoot) === 0)
		{
			$fileName = substr($fileName, strlen($appRoot) + 1);
			$fileName = trim($fileName, '/');
			$fileName = rtrim($this->directory, '/') . '/' . $fileName;
		}

		return $fileName;
	}

	/**
	 * Lists the subdirectories inside an FTP directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool  A list of folders, or false if we could not get a listing
	 *
	 * @throws  \RuntimeException  When the server is incompatible with our FTP folder scanner
	 */
	public function listFolders($dir = null)
	{
		if (!empty($dir))
		{
			$ftpDirectory = $this->translatePath($dir);

			if (!@ftp_chdir($this->connection, $ftpDirectory))
			{
				throw new \RuntimeException(sprintf('Cannot change to FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $ftpDirectory), 500);
			}
		}

		$list = @ftp_rawlist($this->connection, '.');

		if ($list === false)
		{
			throw new \RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
		}

		$folders = array();

		foreach ($list as $v)
		{
			$info = array();
			$vinfo = preg_split("/[\s]+/", $v, 9);

			if ($vinfo[0] !== "total")
			{
				$perms = $vinfo[0];

				if (substr($perms,0,1) == 'd')
				{
					$folders[] = $vinfo[8];
				}
			}
		}

		asort($folders);

		return $folders;
	}
}
