<?php
/**
 * Akeeba Engine
 * The modular PHP5 site backup engine
 * @copyright Copyright (c)2009-2014 Nicholas K. Dionysopoulos
 * @license GNU GPL version 3 or, at your option, any later version
 * @package akeebaengine
 *
 */

// Protection against direct access
defined('AKEEBAENGINE') or die();

class AEPostprocSftp extends AEAbstractPostproc
{
	public function __construct()
	{
		parent::__construct();

		$this->can_delete = true;
		$this->can_download_to_browser = false;
		$this->can_download_to_file = true;
	}

	public function processPart($absolute_filename, $upload_as = null)
	{
		// Retrieve engine configuration data
		$config = AEFactory::getConfiguration();

		$host		= $config->get('engine.postproc.sftp.host', '');
		$port		= $config->get('engine.postproc.sftp.port', 22);
		$user		= $config->get('engine.postproc.sftp.user', '');
		$pass		= $config->get('engine.postproc.sftp.pass', 0);
		$directory	= $config->get('volatile.postproc.directory', null);
		if(empty($directory)) $directory	= $config->get('engine.postproc.sftp.initial_directory', '');

		// You can't fix stupid, but at least you get to shout at them
		if (strtolower(substr($host, 0, 7)) == 'sftp://')
		{
			AEUtilLogger::WriteLog(_AE_LOG_WARNING, 'YOU ARE *** N O T *** SUPPOSED TO ENTER THE sftp:// PROTOCOL PREFIX IN THE FTP HOSTNAME FIELD OF THE Upload to Remote SFTP POST-PROCESSING ENGINE.');
			AEUtilLogger::WriteLog(_AE_LOG_WARNING, 'I am trying to fix your bad configuration setting, but the backup might fail anyway. You MUST fix this in your configuration.');
			$host = substr($host, 7);
		}


		// Process the initial directory
		$directory = '/'.ltrim(trim($directory),'/');

		// Parse tags
		$directory = AEUtilFilesystem::replace_archive_name_variables($directory);
		$config->set('volatile.postproc.directory', $directory);

		// Connect to the SFTP server
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.':: Connecting to remote SFTP');

		$connection = null;
		$sftphandle = null;

		if(!function_exists('ssh2_connect'))
		{
			$this->setWarning("Your web server does not have the SSH2 PHP module, therefore can not connect and upload archives to SFTP servers.");

			return false;
		}

		$connection = ssh2_connect($host, $port);

		if ($connection === false)
		{
			$this->setWarning("Invalid SFTP hostname or port ($host:$port) or the connection is blocked by your web server's firewall.");

			return false;
		}

		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.':: Logging in');
		if(!ssh2_auth_password($connection, $user, $pass))
		{
			$this->setWarning('Could not authenticate access to SFTP server; check your username and password.');

			return false;
		}

		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.':: Establishing an SFTP connection');
		$sftphandle = ssh2_sftp($connection);

		if($sftphandle === false)
		{
			$this->setWarning("Your SSH server does not allow SFTP connections");

			return false;
		}

		// Change to initial directory
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.':: Checking the initial directory');
		if(!$this->_sftp_chdir($directory, $sftphandle))
		{
			$this->setWarning("Invalid initial directory $directory for the remote SFTP server");

			return false;
		}

		$realdir = substr($directory, -1) == '/' ? substr($directory, 0, strlen($directory) - 1) : $directory;
		$basename = empty($upload_as) ? basename($absolute_filename) : $upload_as;
		$realname = $realdir . '/' . $basename;

		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__ . ":: Starting SFTP upload of $absolute_filename");
		$fp = @fopen("ssh2.sftp://{$sftphandle}$realname",'w');

		if ($fp === false)
		{
			$this->setWarning("Could not open remote SFTP file $realname for writing");

			return false;
		}

		$localfp = @fopen($absolute_filename, 'rb');

		if ($localfp === false)
		{
			$this->setWarning("Could not open local file $absolute_filename for reading");
			@fclose($fp);

			return false;
		}

		$res = true;

		while(!feof($localfp) && ($res !== false))
		{
			$buffer = @fread($localfp, 65567);
			$res = @fwrite($fp, $buffer);
		}

		@fclose($fp);
		@fclose($localfp);

		ssh2_exec($connection, 'exit;');
		$connection = null;

		if($res === false)
		{
			// If the file was unreadable, just skip it...
			if(is_readable($absolute_filename))
			{
				$this->setWarning('Uploading '.$realname.' has failed.');

				return false;
			}
			else
			{
				$this->setWarning( 'Uploading '.$realname.' has failed because the source file is unreadable.');

				return true;
			}
		}

		return true;
	}

	public function delete($path)
	{
		// Retrieve engine configuration data
		$config = AEFactory::getConfiguration();

		$host		= $config->get('engine.postproc.sftp.host', '');
		$port		= $config->get('engine.postproc.sftp.port', 21);
		$user		= $config->get('engine.postproc.sftp.user', '');
		$pass		= $config->get('engine.postproc.sftp.pass', 0);

		$directory = dirname($path);

		// Connect to the FTP server
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.'::delete() -- Connecting to remote SFTP');

		$connection = null;
		$sftphandle = null;

		if(!function_exists('ssh2_connect'))
		{
			$this->setWarning("Your web server does not have the SSH2 PHP module, therefore can not connect and upload archives to SFTP servers.");

			return false;
		}

		$connection = ssh2_connect($host, $port);

		if ($connection === false)
		{
			$this->setWarning("Invalid SFTP hostname or port ($host:$port) or the connection is blocked by your web server's firewall.");

			return false;
		}

		if(!ssh2_auth_password($connection, $user, $pass))
		{
			$this->setWarning('Could not authenticate access to SFTP server; check your username and password.');

			return false;
		}

		$sftphandle = ssh2_sftp($connection);

		if($sftphandle === false)
		{
			$this->setWarning("Your SSH server does not allow SFTP connections");

			return false;
		}

		// Change to initial directory
		if(!$this->_sftp_chdir($directory, $sftphandle))
		{
			$this->setWarning("Invalid initial directory $directory for the remote SFTP server");

			return false;
		}

		try
		{
			$res = @ssh2_sftp_unlink($sftphandle, $path);
		}
		catch(Exception $e)
		{
			// Funny how PHP dies without returning false if you don't use a try/catch statement, eh?
			$res = false;

			$this->setWarning($e->getMessage());
		}

		ssh2_exec($connection, 'exit;');
		$connection = null;

		if(!$res)
		{
			$this->setWarning('Deleting '.$path.' has failed.');

			return false;
		}
		else
		{
			return true;
		}
	}

	public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null)
	{
		// Retrieve engine configuration data
		$config = AEFactory::getConfiguration();

		$host		= $config->get('engine.postproc.sftp.host', '');
		$port		= $config->get('engine.postproc.sftp.port', 21);
		$user		= $config->get('engine.postproc.sftp.user', '');
		$pass		= $config->get('engine.postproc.sftp.pass', 0);

		$directory = dirname($remotePath);

		// Connect to the FTP server
		AEUtilLogger::WriteLog(_AE_LOG_DEBUG, __CLASS__.'::delete() -- Connecting to remote SFTP');

		$connection = null;
		$sftphandle = null;

		if(!function_exists('ssh2_connect'))
		{
			$this->setWarning("Your web server does not have the SSH2 PHP module, therefore can not connect and upload archives to SFTP servers.");

			return false;
		}

		$connection = ssh2_connect($host, $port);

		if ($connection === false)
		{
			$this->setWarning("Invalid SFTP hostname or port ($host:$port) or the connection is blocked by your web server's firewall.");

			return false;
		}

		if(!ssh2_auth_password($connection, $user, $pass))
		{
			$this->setWarning('Could not authenticate access to SFTP server; check your username and password.');

			return false;
		}

		$sftphandle = ssh2_sftp($connection);

		if($sftphandle === false)
		{
			$this->setWarning("Your SSH server does not allow SFTP connections");

			return false;
		}

		// Change to initial directory
		if(!$this->_sftp_chdir($directory, $sftphandle))
		{
			$this->setWarning("Invalid initial directory $directory for the remote SFTP server");

			return false;
		}



		$fp = @fopen("ssh2.sftp://{$sftphandle}$remotePath",'rb');

		if ($fp === false)
		{
			$this->setWarning("Could not open remote SFTP file $remotePath for reading");

			return false;
		}

		$localfp = @fopen($localFile, 'wb');

		if ($localfp === false)
		{
			$this->setWarning("Could not open local file $localFile for reading");
			@fclose($fp);

			return false;
		}

		$res = true;

		while(!feof($fp) && ($res !== false))
		{
			$buffer = @fread($fp, 65567);
			$res = @fwrite($localFile, $buffer);
		}

		@fclose($fp);
		@fclose($localfp);

		ssh2_exec($connection, 'exit;');
		$connection = null;

		if($res === false)
		{
			$this->setWarning( 'Downloading ' . $remotePath . ' has failed because the source file is unreadable.');

			return false;
		}

		return true;
	}

	/**
	 * Changes to the requested directory in the remote server. You give only the
	 * path relative to the initial directory and it does all the rest by itself,
	 * including doing nothing if the remote directory is the one we want. If the
	 * directory doesn't exist, it creates it.
	 *
	 * @param   string    $dir
	 * @param   resource  $sftphandle
	 *
	 * @return  boolean
	 */
	private function _sftp_chdir($dir, &$sftphandle)
	{
		// Calculate "real" (absolute) SFTP path

		$result = @ssh2_sftp_stat($sftphandle, $dir);

		if($result === false)
		{
			// The directory doesn't exist, let's try to create it...
			if(!$this->_makeDirectory($dir, $sftphandle))
			{
				return false;
			}
		}

		// Update the private "current remote directory" variable
		return true;
	}

	/**
	 * Creates a nested directory structure on the remote SFTP server
	 *
	 * @param   string    $dir
	 * @param   resource  $sftphandle
	 *
	 * @return  boolean
	 */
	private function _makeDirectory($dir, &$sftphandle)
	{
		$alldirs = explode('/', $dir);
		$previousDir = '';

		foreach($alldirs as $curdir)
		{
			$check = $previousDir.'/'.$curdir;

			if(!@ssh2_sftp_stat($sftphandle, $check) )
			{
				if(ssh2_sftp_mkdir($sftphandle, $check, 0755, true) === false)
				{
					$this->setWarning('Could not create SFTP directory ' . $check);

					return false;
				}
			}

			$previousDir = $check;
		}

		return true;
	}
}