<?php
/**
 * WordPress Filesystem Class for implementing SSH2
 *
 * To use this class you must follow these steps for PHP 5.2.6+
 *
 * @contrib http://kevin.vanzonneveld.net/techblog/article/make_ssh_connections_with_php/ - Installation Notes
 *
 * Complie libssh2 (Note: Only 0.14 is officaly working with PHP 5.2.6+ right now, But many users have found the latest versions work)
 *
 * cd /usr/src
 * wget http://surfnet.dl.sourceforge.net/sourceforge/libssh2/libssh2-0.14.tar.gz
 * tar -zxvf libssh2-0.14.tar.gz
 * cd libssh2-0.14/
 * ./configure
 * make all install
 *
 * Note: Do not leave the directory yet!
 *
 * Enter: pecl install -f ssh2
 *
 * Copy the ssh.so file it creates to your PHP Module Directory.
 * Open up your PHP.INI file and look for where extensions are placed.
 * Add in your PHP.ini file: extension=ssh2.so
 *
 * Restart Apache!
 * Check phpinfo() streams to confirm that: ssh2.shell, ssh2.exec, ssh2.tunnel, ssh2.scp, ssh2.sftp  exist.
 *
 * Note: as of WordPress 2.8, This utilises the PHP5+ function 'stream_get_contents'
 *
 * @since 2.7.0
 *
 * @package WordPress
 * @subpackage Filesystem
 */
class WP_Filesystem_SSH2 extends WP_Filesystem_Base {

	/**
	 */
	public $link = false;

	/**
	 * @var resource
	 */
	public $sftp_link;
	public $keys = false;

	/**
	 * @param array $opt
	 */
	public function __construct( $opt = '' ) {
		$this->method = 'ssh2';
		$this->errors = new WP_Error();

		//Check if possible to use ssh2 functions.
		if ( ! extension_loaded( 'ssh2' ) ) {
			$this->errors->add( 'no_ssh2_ext', __( 'The ssh2 PHP extension is not available' ) );
			return;
		}
		if ( ! function_exists( 'stream_get_contents' ) ) {
			$this->errors->add(
				'ssh2_php_requirement',
				sprintf(
					/* translators: %s: stream_get_contents() */
					__( 'The ssh2 PHP extension is available, however, we require the PHP5 function %s' ),
					'<code>stream_get_contents()</code>'
				)
			);
			return;
		}

		// Set defaults:
		if ( empty( $opt['port'] ) ) {
			$this->options['port'] = 22;
		} else {
			$this->options['port'] = $opt['port'];
		}

		if ( empty( $opt['hostname'] ) ) {
			$this->errors->add( 'empty_hostname', __( 'SSH2 hostname is required' ) );
		} else {
			$this->options['hostname'] = $opt['hostname'];
		}

		// Check if the options provided are OK.
		if ( ! empty( $opt['public_key'] ) && ! empty( $opt['private_key'] ) ) {
			$this->options['public_key']  = $opt['public_key'];
			$this->options['private_key'] = $opt['private_key'];

			$this->options['hostkey'] = array( 'hostkey' => 'ssh-rsa' );

			$this->keys = true;
		} elseif ( empty( $opt['username'] ) ) {
			$this->errors->add( 'empty_username', __( 'SSH2 username is required' ) );
		}

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

		if ( empty( $opt['password'] ) ) {
			// Password can be blank if we are using keys.
			if ( ! $this->keys ) {
				$this->errors->add( 'empty_password', __( 'SSH2 password is required' ) );
			}
		} else {
			$this->options['password'] = $opt['password'];
		}
	}

	/**
	 * @return bool
	 */
	public function connect() {
		if ( ! $this->keys ) {
			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'] );
		} else {
			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'], $this->options['hostkey'] );
		}

		if ( ! $this->link ) {
			$this->errors->add(
				'connect',
				/* translators: %s: hostname:port */
				sprintf(
					__( 'Failed to connect to SSH2 Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);
			return false;
		}

		if ( ! $this->keys ) {
			if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) {
				$this->errors->add(
					'auth',
					/* translators: %s: username */
					sprintf(
						__( 'Username/Password incorrect for %s' ),
						$this->options['username']
					)
				);
				return false;
			}
		} else {
			if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'], $this->options['private_key'], $this->options['password'] ) ) {
				$this->errors->add(
					'auth',
					/* translators: %s: username */
					sprintf(
						__( 'Public and Private keys incorrect for %s' ),
						$this->options['username']
					)
				);
				return false;
			}
		}

		$this->sftp_link = ssh2_sftp( $this->link );
		if ( ! $this->sftp_link ) {
			$this->errors->add(
				'connect',
				/* translators: %s: hostname:port */
				sprintf(
					__( 'Failed to initialize a SFTP subsystem session with the SSH2 Server %s' ),
					$this->options['hostname'] . ':' . $this->options['port']
				)
			);
			return false;
		}

		return true;
	}

	/**
	 * Gets the ssh2.sftp PHP stream wrapper path to open for the given file.
	 *
	 * This method also works around a PHP bug where the root directory (/) cannot
	 * be opened by PHP functions, causing a false failure. In order to work around
	 * this, the path is converted to /./ which is semantically the same as /
	 * See https://bugs.php.net/bug.php?id=64169 for more details.
	 *
	 * @since 4.4.0
	 *
	 * @param string $path The File/Directory path on the remote server to return
	 * @return string The ssh2.sftp:// wrapped path to use.
	 */
	public function sftp_path( $path ) {
		if ( '/' === $path ) {
			$path = '/./';
		}
		return 'ssh2.sftp://' . $this->sftp_link . '/' . ltrim( $path, '/' );
	}

	/**
	 * @param string $command
	 * @param bool $returnbool
	 * @return bool|string True on success, false on failure. String if the command was executed, `$returnbool`
	 *                     is false (default), and data from the resulting stream was retrieved.
	 */
	public function run_command( $command, $returnbool = false ) {
		if ( ! $this->link ) {
			return false;
		}

		if ( ! ( $stream = ssh2_exec( $this->link, $command ) ) ) {
			$this->errors->add(
				'command',
				/* translators: %s: command */
				sprintf(
					__( 'Unable to perform command: %s' ),
					$command
				)
			);
		} else {
			stream_set_blocking( $stream, true );
			stream_set_timeout( $stream, FS_TIMEOUT );
			$data = stream_get_contents( $stream );
			fclose( $stream );

			if ( $returnbool ) {
				return ( $data === false ) ? false : '' != trim( $data );
			} else {
				return $data;
			}
		}
		return false;
	}

	/**
	 * @param string $file
	 * @return string|false
	 */
	public function get_contents( $file ) {
		return file_get_contents( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @return array
	 */
	public function get_contents_array( $file ) {
		return file( $this->sftp_path( $file ) );
	}

	/**
	 * @param string   $file
	 * @param string   $contents
	 * @param bool|int $mode
	 * @return bool
	 */
	public function put_contents( $file, $contents, $mode = false ) {
		$ret = file_put_contents( $this->sftp_path( $file ), $contents );

		if ( $ret !== strlen( $contents ) ) {
			return false;
		}

		$this->chmod( $file, $mode );

		return true;
	}

	/**
	 * @return bool
	 */
	public function cwd() {
		$cwd = ssh2_sftp_realpath( $this->sftp_link, '.' );
		if ( $cwd ) {
			$cwd = trailingslashit( trim( $cwd ) );
		}
		return $cwd;
	}

	/**
	 * @param string $dir
	 * @return bool|string
	 */
	public function chdir( $dir ) {
		return $this->run_command( 'cd ' . $dir, true );
	}

	/**
	 * @param string $file
	 * @param string $group
	 * @param bool   $recursive
	 *
	 * @return bool
	 */
	public function chgrp( $file, $group, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}
		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
		}
		return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
	}

	/**
	 * @param string $file
	 * @param int    $mode
	 * @param bool   $recursive
	 * @return bool|string
	 */
	public function chmod( $file, $mode = false, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}

		if ( ! $mode ) {
			if ( $this->is_file( $file ) ) {
				$mode = FS_CHMOD_FILE;
			} elseif ( $this->is_dir( $file ) ) {
				$mode = FS_CHMOD_DIR;
			} else {
				return false;
			}
		}

		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chmod %o %s', $mode, escapeshellarg( $file ) ), true );
		}
		return $this->run_command( sprintf( 'chmod -R %o %s', $mode, escapeshellarg( $file ) ), true );
	}

	/**
	 * Change the ownership of a file / folder.
	 *
	 * @param string     $file      Path to the file.
	 * @param string|int $owner     A user name or number.
	 * @param bool       $recursive Optional. If set True changes file owner recursivly. Default False.
	 * @return bool True on success or false on failure.
	 */
	public function chown( $file, $owner, $recursive = false ) {
		if ( ! $this->exists( $file ) ) {
			return false;
		}
		if ( ! $recursive || ! $this->is_dir( $file ) ) {
			return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
		}
		return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
	}

	/**
	 * @param string $file
	 * @return string|false
	 */
	public function owner( $file ) {
		$owneruid = @fileowner( $this->sftp_path( $file ) );
		if ( ! $owneruid ) {
			return false;
		}
		if ( ! function_exists( 'posix_getpwuid' ) ) {
			return $owneruid;
		}
		$ownerarray = posix_getpwuid( $owneruid );
		return $ownerarray['name'];
	}

	/**
	 * @param string $file
	 * @return string
	 */
	public function getchmod( $file ) {
		return substr( decoct( @fileperms( $this->sftp_path( $file ) ) ), -3 );
	}

	/**
	 * @param string $file
	 * @return string|false
	 */
	public function group( $file ) {
		$gid = @filegroup( $this->sftp_path( $file ) );
		if ( ! $gid ) {
			return false;
		}
		if ( ! function_exists( 'posix_getgrgid' ) ) {
			return $gid;
		}
		$grouparray = posix_getgrgid( $gid );
		return $grouparray['name'];
	}

	/**
	 * @param string   $source
	 * @param string   $destination
	 * @param bool     $overwrite
	 * @param int|bool $mode
	 * @return bool
	 */
	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
		if ( ! $overwrite && $this->exists( $destination ) ) {
			return false;
		}
		$content = $this->get_contents( $source );
		if ( false === $content ) {
			return false;
		}
		return $this->put_contents( $destination, $content, $mode );
	}

	/**
	 * @param string $source
	 * @param string $destination
	 * @param bool   $overwrite
	 * @return bool
	 */
	public function move( $source, $destination, $overwrite = false ) {
		return @ssh2_sftp_rename( $this->sftp_link, $source, $destination );
	}

	/**
	 * @param string      $file
	 * @param bool        $recursive
	 * @param string|bool $type
	 * @return bool
	 */
	public function delete( $file, $recursive = false, $type = false ) {
		if ( 'f' == $type || $this->is_file( $file ) ) {
			return ssh2_sftp_unlink( $this->sftp_link, $file );
		}
		if ( ! $recursive ) {
			return ssh2_sftp_rmdir( $this->sftp_link, $file );
		}
		$filelist = $this->dirlist( $file );
		if ( is_array( $filelist ) ) {
			foreach ( $filelist as $filename => $fileinfo ) {
				$this->delete( $file . '/' . $filename, $recursive, $fileinfo['type'] );
			}
		}
		return ssh2_sftp_rmdir( $this->sftp_link, $file );
	}

	/**
	 * @param string $file
	 * @return bool
	 */
	public function exists( $file ) {
		return file_exists( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @return bool
	 */
	public function is_file( $file ) {
		return is_file( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $path
	 * @return bool
	 */
	public function is_dir( $path ) {
		return is_dir( $this->sftp_path( $path ) );
	}

	/**
	 * @param string $file
	 * @return bool
	 */
	public function is_readable( $file ) {
		return is_readable( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @return bool
	 */
	public function is_writable( $file ) {
		// PHP will base it's writable checks on system_user === file_owner, not ssh_user === file_owner
		return true;
	}

	/**
	 * @param string $file
	 * @return int
	 */
	public function atime( $file ) {
		return fileatime( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @return int
	 */
	public function mtime( $file ) {
		return filemtime( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @return int
	 */
	public function size( $file ) {
		return filesize( $this->sftp_path( $file ) );
	}

	/**
	 * @param string $file
	 * @param int    $time
	 * @param int    $atime
	 */
	public function touch( $file, $time = 0, $atime = 0 ) {
		//Not implemented.
	}

	/**
	 * @param string $path
	 * @param mixed  $chmod
	 * @param mixed  $chown
	 * @param mixed  $chgrp
	 * @return bool
	 */
	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
		$path = untrailingslashit( $path );
		if ( empty( $path ) ) {
			return false;
		}

		if ( ! $chmod ) {
			$chmod = FS_CHMOD_DIR;
		}
		if ( ! ssh2_sftp_mkdir( $this->sftp_link, $path, $chmod, true ) ) {
			return false;
		}
		if ( $chown ) {
			$this->chown( $path, $chown );
		}
		if ( $chgrp ) {
			$this->chgrp( $path, $chgrp );
		}
		return true;
	}

	/**
	 * @param string $path
	 * @param bool   $recursive
	 * @return bool
	 */
	public function rmdir( $path, $recursive = false ) {
		return $this->delete( $path, $recursive );
	}

	/**
	 * @param string $path
	 * @param bool   $include_hidden
	 * @param bool   $recursive
	 * @return bool|array
	 */
	public function dirlist( $path, $include_hidden = true, $recursive = false ) {
		if ( $this->is_file( $path ) ) {
			$limit_file = basename( $path );
			$path       = dirname( $path );
		} else {
			$limit_file = false;
		}

		if ( ! $this->is_dir( $path ) ) {
			return false;
		}

		$ret = array();
		$dir = @dir( $this->sftp_path( $path ) );

		if ( ! $dir ) {
			return false;
		}

		while ( false !== ( $entry = $dir->read() ) ) {
			$struc         = array();
			$struc['name'] = $entry;

			if ( '.' == $struc['name'] || '..' == $struc['name'] ) {
				continue; //Do not care about these folders.
			}

			if ( ! $include_hidden && '.' == $struc['name'][0] ) {
				continue;
			}

			if ( $limit_file && $struc['name'] != $limit_file ) {
				continue;
			}

			$struc['perms']       = $this->gethchmod( $path . '/' . $entry );
			$struc['permsn']      = $this->getnumchmodfromh( $struc['perms'] );
			$struc['number']      = false;
			$struc['owner']       = $this->owner( $path . '/' . $entry );
			$struc['group']       = $this->group( $path . '/' . $entry );
			$struc['size']        = $this->size( $path . '/' . $entry );
			$struc['lastmodunix'] = $this->mtime( $path . '/' . $entry );
			$struc['lastmod']     = date( 'M j', $struc['lastmodunix'] );
			$struc['time']        = date( 'h:i:s', $struc['lastmodunix'] );
			$struc['type']        = $this->is_dir( $path . '/' . $entry ) ? 'd' : 'f';

			if ( 'd' == $struc['type'] ) {
				if ( $recursive ) {
					$struc['files'] = $this->dirlist( $path . '/' . $struc['name'], $include_hidden, $recursive );
				} else {
					$struc['files'] = array();
				}
			}

			$ret[ $struc['name'] ] = $struc;
		}
		$dir->close();
		unset( $dir );
		return $ret;
	}
}
