<?php
defined("ABSPATH") or die("");
class DUP_PRO_FTPcURL
{
	/** @var string FTP host */
	private $server;

	/** @var string FTP port */
	private $port = 21;

	/** @var string FTP username */
	private $username;

	/** @var string FTP password */
	private $password;

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

	/** @var integer timeout in seconds */
	private $timeout_in_sec = 90;

	/** @var boolean whether FTP is ssl ftp or not */
	private $ssl = false;

	/** @var boolean whether FTP is passive mode or not */
	private $passive_mode = false;

	/**
	 * class constructor
	 *
	 * @param string $server FTP host
	 * @param integer $port FTP port
	 * @param string $username FTP username
	 * @param string $password FTP password
	 * @param string $directory remote FTP directory
	 * @param integer $timeout_in_sec timeout in seconds
	 * @param boolean $ssl whether it is ssl ftp or not
	 * @param boolean $passive_mode whether it is passive mode or not
	 * 
	 * @return void
	 */
	public function __construct($server, $port = 21, $username = 'anonymous', $password = 'anonymous@gmail.com', $directory = '/', $timeout_in_sec = 15, $ssl = false, $passive_mode = false)
	{
		if (empty($server)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $server argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}

		if (!is_string($server)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $server argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $server);
		}

		if (empty($port)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $port argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}
		$port = (int) $port;
		if ($port <= 0) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $port argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $port);
		}

		if (empty($username)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $username argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}
		if (!is_string($username)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $username argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $username);
		}

		if (empty($password)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $password argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}
		if (!is_string($password)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $password argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $password);
		}

		if (empty($directory)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $directory argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}
		if (!is_string($directory)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $directory argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $directory);
		}

		if (empty($timeout_in_sec)) {
			throw new InvalidArgumentException(DUP_PRO_U::esc_html__('Invalid $timeout_in_sec argument with the empty in DUP_PRO_FTPcURL class constructor.'));
		}
		$timeout_in_sec = (int) $timeout_in_sec;
		if ($timeout_in_sec <= 0) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $timeout_in_sec argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $timeout_in_sec);
		}

		if (!is_bool($ssl)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $ssl argument with the value %s in DUP_PRO_FTPcURL class constructor.')), $ssl);
		}

		if (!is_bool($passive_mode)) {
			throw new InvalidArgumentException(sprintf(DUP_PRO_U::esc_html__('Invalid $passive_mode argument with the value %s  in DUP_PRO_FTPcURL class constructor.')), $passive_mode);
		}

		$this->server = $server;
		$this->port = $port;
		$this->username = $username;
		$this->password = $password;
		$this->directory = $directory;
		$this->timeout_in_sec = $timeout_in_sec;
		$this->ssl = $ssl;
		$this->passive_mode = $passive_mode;
	}

	/**
	 * curl call
	 *
	 * @param string $path where the curl call occur
	 * @param array $options configuration options
	 *
	 * @throws Exception upon error
	 * @return void
	 */
	private function curl_call($path, $options = array())
	{
		$ch = curl_init();
		$url = sprintf('ftp://%s:%d/%s', $this->server, $this->port, $this->untrailing_left_slash_path($path));
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_USERPWD, sprintf('%s:%s', $this->username, $this->password));

		curl_setopt($ch, CURLOPT_HEADER, false);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout_in_sec);
		curl_setopt($ch, CURLOPT_TIMEOUT, 0);

		if ($this->ssl) {
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
			curl_setopt($ch, CURLOPT_FTP_SSL, CURLFTPSSL_TRY);
			curl_setopt($ch, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS);
		} else {
			// To do based on settings verify peer
			$global = DUP_PRO_Global_Entity::get_instance();

			$setopts[CURLOPT_SSL_VERIFYPEER] = $global->ssl_disableverify ? false : true;
			$setopts[CURLOPT_SSL_VERIFYHOST] = $global->ssl_disableverify ? 0 : 2;
			if (!$global->ssl_useservercerts) {
				$setopts[CURLOPT_CAINFO] = DUPLICATOR_PRO_CERT_PATH;
				$setopts[CURLOPT_CAPATH] = DUPLICATOR_PRO_CERT_PATH;
			}
		}

		if ($this->passive_mode) {
			curl_setopt($ch, CURLOPT_FTP_USE_EPSV, true);
		} else {
			curl_setopt($ch, CURLOPT_FTP_USE_EPRT, true);
			curl_setopt($ch, CURLOPT_FTPPORT, 0);
		}

		foreach ($options as $name => $value) {
			curl_setopt($ch, $name, $value);
		}

		if (($response = curl_exec($ch)) === false) {
			if (($errno = curl_errno($ch))) {
				switch ($errno) {
					case 6:
					case 7:
						throw new Exception(DUP_PRO_U::esc_html__('Unable to connect to FTP server. Please check your FTP hostname, port, and active mode settings.'));
					case 9:
						throw new Exception(DUP_PRO_U::esc_html__('Unable to change FTP directory. Please ensure that you have permission on the server.'));
					case 23:
						throw new Exception(DUP_PRO_U::esc_html__('Unable to download file from FTP server. Please ensure that you have enough disk space.'));
					case 28:
						throw new Exception(DUP_PRO_U::esc_html__('Connecting to FTP server timed out. Please check FTP hostname, port, username, password, and active mode settings.'));
					case 67:
						throw new Exception(DUP_PRO_U::esc_html__('Unable to login to FTP server. Please check your username and password.'));
					default:
						throw new Exception(sprintf(DUP_PRO_U::esc_html__('Unable to connect to FTP. Error code: %s.'), $errno));
				}
			}
		}

		$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		if ($http_code >= 400) {
			if (isset($this->messages[$http_code])) {
				throw new Exception(sprintf(DUP_PRO_U::esc_html__('Error code: %s.'), $this->messages[ $http_code ]));
			} else {
				throw new Exception(sprintf(DUP_PRO_U::esc_html__('Error code: %s.'), $http_code));
			}
		}

		return $response;
	}

	/**
	 * test ftp connection
	 *
	 * @throws Exception upon error
	 * @return boolean
	 */
	public function test_conn() {
		try {
			$this->curl_call(
				'/',
				array(
					CURLOPT_TIMEOUT => $this->timeout_in_sec,
				)
			);
		} catch (Exception $e) {
			$message = sprintf(DUP_PRO_U::esc_html__('Error connecting to FTP server %1$s:%2$d.  Exception Error: %3$s.'), $this->server, $this->port, $e->getMessage());
            DUP_PRO_LOG::trace($message);
			throw $e;
		}
		return true;
	}

	/**
	 * create a dir
	 *
	 * @param string $folder_path a dir path which need to create
	 *
	 * @throws Exception upon error
	 * @return boolean
	 */
	public function create_directory($folder_path)
	{
		try {
			$this->curl_call(
				$folder_path,
				array(
					CURLOPT_FTP_CREATE_MISSING_DIRS => true,
				)
			);
		} catch(Exception $e) {
			DUP_PRO_LOG::trace("Error creating folder when using FTP cURL. FTP Info: " . $this->get_info().' Exception Error: ' . $e->getMessage());
			return false;
		}
		DUP_PRO_LOG::trace("Successfully created folder by FTP cURL.");
		return true;
	}

	/**
	 * upload file
	 *
	 * @param string $local_file_path local file path
	 * @param string $remote_file_path remote file path
	 * @param string $dest_filename destination file name
	 *
	 * @throws Exception upon error
	 * @return void
	 */
	public function upload_file($local_file_path, $remote_file_path, $dest_filename = '')
	{
		$remote_file_path = $this->untrailing_left_slash_path($remote_file_path);
		$file_size = filesize($local_file_path);
		// Upload file from file stream
		if (($file_stream = fopen($local_file_path, 'rb'))) {
			$options = array(
				CURLOPT_UPLOAD => true,
				// by chance If FTP curl storage set and In btw a user has deleted storage dir from a FTP program
				CURLOPT_FTP_CREATE_MISSING_DIRS => true,
				CURLOPT_INFILE => $file_stream,
				CURLOPT_INFILESIZE => $file_size,
				CURLOPT_NOPROGRESS => true,
			);
			$this->curl_call(sprintf('/%s/%s', $this->directory, $remote_file_path), $options);
			fclose( $file_stream );
		}
		return true;
	}

	/**
	 * download file
	 *
	 * @param string $remote_file_path
	 * @param string $local either local file path or local dir, depends on the $is_local_directory
	 * @param boolean $is_local_directory whether $local is local directory or full path to download
	 * 
	 * @throws Exception upon error
	 * @return boolean
	 */
	public function download_file($remote_file_path, $local, $is_local_directory = true)
	{
		$remote_file_path = $this->untrailing_left_slash_path($remote_file_path);

		if ($is_local_directory) {
			$filename = basename($remote_file_path);
			$local_file_path = "$local/$filename";
		} else {
			$local_file_path = $local;
		}

		if (($file_stream = fopen($local_file_path, 'wb'))) {
			try {
				$options = array(
					CURLOPT_FILE => $file_stream,
					CURLOPT_NOPROGRESS => true,
				);
				$this->curl_call(sprintf('/%s/%s', $this->directory, $remote_file_path), $options);
			} catch (Exception $e) {
				DUP_PRO_LOG::trace("Error downloading " . $remote_source_filepath . " into " . $local_dest_filepath . ". FTP Info: " . $this->get_info().' Exception Error: ' . $e->getMessage());
				throw $e;
			}

			fclose($file_stream);
		}

		return true;
	}

	/**
	 * delete remote file
	 *
	 * @param string $file_path remote file path
	 * 
	 * @throws Exception upon error
	 * @return boolean
	 */
	public function delete($file_path)
	{
		$file_path = $this->untrailing_left_slash_path($file_path);
		try {
			$this->curl_call(
				sprintf('/%s/', $this->directory),
				array(
					CURLOPT_QUOTE => array(sprintf('DELE /%s', $this->untrailing_left_slash_path(sprintf('/%s/%s', $this->directory, $file_path)))),
				)
			);
		} catch (Exception $e) {
			DUP_PRO_LOG::trace("Failed to delete ".$file_path." from " . $this->server . ". Error code: " . $e->getCode() . ". Error Message:" . $e->getMessage());
			return false;
		}
		DUP_PRO_LOG::trace("Successfully deleted ".$file_path." from " . $this->server);
		return true;
	}

	/**
	 * upload file by chunking mode
	 *
	 * @param string $source_filepath local file path
	 * @param string $storage_folder remote folder
	 * @param integer $max_upload_time_in_sec max upload time in sec in a chunk
	 * @param integer $offset file offset
	 * @param integer $server_load_delay
	 * @param string $dest_filename remote file name
	 * @return object
	 */
	public function upload_chunk($source_filepath, $storage_folder, $max_upload_time_in_sec = 15, $offset = 0, $server_load_delay = 0,  $dest_filename = '')
	{
		DUP_PRO_LOG::trace("----------------------------");
		DUP_PRO_LOG::trace("FTP CURL CHUNK UPLOAD START");
		DUP_PRO_LOG::trace("FTP CHUNK OFFSET IN=$offset");
		/* @var $ftp_upload_info DUP_PRO_FTP_UploadInfo */
		$ftp_upload_info = new DUP_PRO_FTP_UploadInfo();
		$start_time = time();

		$remote_file_path = !empty($dest_filename) ? $dest_filename : basename($source_filepath);
		try {
			DUP_PRO_LOG::trace("call upload chunk for FTP cURL");
			$fp = fopen($source_filepath, 'rb');
			if ($fp) {
				// Read file chunk data
				while (fseek($fp, $offset) !== -1
			&& $file_chunk_data = fread($fp, DUPLICATOR_PRO_FTP_CURL_CHUNK_SIZE)) {
					$ret = $this->upload_file_chunk($file_chunk_data, $remote_file_path, $offset);
					if ($ret) {
						$offset = ftell($fp);
					}

					$time_passed = time() - $start_time;
					if ($time_passed < $max_upload_time_in_sec) {
						$offset = ftell($fp);
						DUP_PRO_LOG::trace("Timed out $max_upload_time_in_sec so saving off offset $offset");
						break;
					}
					if($server_load_delay !== 0) {
                        usleep($server_load_delay);
                    }
				}
				$ftp_upload_info->next_offset = $offset;
			} else {
				$message = sprintf(DUP_PRO_U::esc_html__('Error opening %1$ for cURL FTP'), $source_filepath);
				DUP_PRO_LOG::trace($message);
				$ftp_upload_info->error_details = $message;
				$ftp_upload_info->next_offset = $offset;
			}
			DUP_PRO_LOG::trace("closing local file handle");
			fclose($fp);

			$source_filesize = filesize($source_filepath);
			if ($source_filesize <= $ftp_upload_info->next_offset) {
				$ftp_size = $this->get_file_size($remote_file_path);

				if (!$ftp_size && $ftp_size != $source_filesize) {
					$error_message = sprintf(DUP_PRO_U::esc_html__('cURL FTP size mismatch for %1$s. Local file=%2$d bytes while server\'s file is %3$d bytes.'), $source_filepath, $source_filesize, $ftp_size);

					DUP_PRO_LOG::trace($error_message);
					$ftp_upload_info->error_details = $error_message;
					$ftp_upload_info->fatal_error = true;

					$this->delete($remote_file_path);
				} else {
					DUP_PRO_LOG::trace("FTP cURL upload file finish");
					$ftp_upload_info->success = true;
				}
			} else {
				DUP_PRO_LOG::trace("FTP cURL upload file is not finished yet..");
			}
		} catch (Exception $e) {
			$message = "Tried to upload file when connection wasn't opened. Info:" . $this->get_info() .' Exception Error: '. $e->getMessage();
			$ftp_upload_info->error_details = $message;
			DUP_PRO_LOG::trace($message);
			$ftp_upload_info->next_offset = $offset;
		}
		DUP_PRO_LOG::trace("FTP CURL CHUNK UPLOAD END");
		DUP_PRO_LOG::trace("----------------------------");

		return $ftp_upload_info;
	}

	/**
	 * upload file chunk with given data
	 *
	 * @param string $file_chunk_data
	 * @param string $remote_file_path
	 * @param integer $file_range_start
	 *
	 * @throws Exception upon error
	 * @return boolean
	 */
	private function upload_file_chunk($file_chunk_data, $remote_file_path, $file_range_start = 0)
	{
		$remote_file_path = $this->untrailing_left_slash_path($remote_file_path);

		if (($file_chunk_stream = fopen('php://temp', 'wb+')) === false) {
			return true;
		}

		if (($file_chunk_size = fwrite($file_chunk_stream, $file_chunk_data)) === false) {
			return true;
		}
				
		rewind($file_chunk_stream);
		$this->curl_call(
			sprintf('/%s/%s', $this->directory, $remote_file_path),
			array(
				CURLOPT_UPLOAD => true,
				CURLOPT_FTPAPPEND  => true,
				CURLOPT_INFILE => $file_chunk_stream,
				CURLOPT_INFILESIZE => $file_chunk_size,
			)
		);
		fclose($file_chunk_stream);
		return true;
	}

	/**
	 * get remote file size
	 *
	 * @param string $remote_file_name
	 * @return boolean|integer gives size of remote file or false on failure
	 */
	public function get_file_size($remote_file_name)
	{
		$items = $this->raw_list_folder($this->directory);
		foreach ($items as $item) {
			if ($meta = preg_split('/\s+/', $item)) {
				if (!empty($meta[8]) && !empty($meta[4]) && $meta[8] == $remote_file_name) {
					return $meta[4];
				}
			}
		}
		return false;
	}

	/**
	 * get information of the FTP
	 *
	 * @return string
	 */
	private function get_info()
	{
		$ssl_string = DUP_PRO_STR::boolToString($this->ssl);
		$passive_string = DUP_PRO_STR::boolToString($this->passive_mode);

		return sprintf(DUP_PRO_U::esc_html__('Server:%1$s Port:%2$d User:%3$s Directory:%4$s SSL:%5$s Passive:%6$s.'), $this->server, $this->port, $this->username, $this->directory, $ssl_string, $passive_string);
	}

	/**
	 * get all files in remote folder
	 *
	 * @param string $folder_path remote folder path
	 *
	 * @throws Exception upon error
	 * @return array
	 */
	public function get_filelist($folder_path)
	{
		$items = $this->raw_list_folder($folder_path);
		$files = array();
		foreach ($items as $item) {
			if ($meta = preg_split('/\s+/', $item)) {
				if (!empty($meta[8])) {
					$files[] = trim($meta[8]);
				}
			}
		}
		DUP_PRO_LOG::trace("Get file list from cURL FTP: " . print_r($files, true));
		return $files;
	}

	/**
	 * get all files meta information 
	 *
	 * @param string $folder_path remote dir path
	 *
	 * @throws Exception upon error
	 * @return array
	 */
	private function raw_list_folder($folder_path)
	{
		$folder_path = $this->untrailing_left_slash_path($folder_path);
		$items = $this->curl_call(
			sprintf('/%s/', $folder_path),
			array(
				CURLOPT_CUSTOMREQUEST => 'LIST -tlA',
			)
		);
		return explode("\n", $items);
	}

	/**
	 * clean path with removing left slash
	 *
	 * @param string $path
	 * @return string
	 */
	private function untrailing_left_slash_path($path)
	{
	 	return ltrim(preg_replace('/[\\\\\/]+/', '/', $path), '/');
	}
}
