drupal/includes/filetransfer/filetransfer.inc

339 lines
8.9 KiB
PHP
Raw Normal View History

<?php
// $Id$
/*
* Base FileTransfer class.
*
* Classes extending this class perform file operations on directories not
* writable by the webserver. To achieve this, the class should connect back
* to the server using some backend (for example FTP or SSH). To keep security,
* the password should always be asked from the user and never stored. For
* safety, all methods operate only inside a "jail", by default the Drupal root.
*/
abstract class FileTransfer {
protected $username;
protected $password;
protected $hostname = 'localhost';
protected $port;
/**
* The constructor for the UpdateConnection class. This method is also called
* from the classes that extend this class and override this method.
*/
function __construct($jail) {
$this->jail = $jail;
}
abstract static function factory($jail, $settings);
/**
* Implementation of the magic __get() method.
*
* If the connection isn't set to anything, this will call the connect() method
* and set it to and return the result; afterwards, the connection will be
* returned directly without using this method.
*/
function __get($name) {
if ($name == 'connection') {
$this->connect();
return $this->connection;
}
if ($name == 'chroot') {
$this->setChroot();
return $this->chroot;
}
}
/**
* Connect to the server.
*/
abstract protected function connect();
/**
* Copies a directory.
*
* @param $source
* The source path.
* @param $destination
* The destination path.
*/
public final function copyDirectory($source, $destination) {
$source = $this->sanitizePath($source);
$destination = $this->fixRemotePath($destination);
$this->checkPath($destination);
$this->copyDirectoryJailed($source, $destination);
}
/**
* @see http://php.net/chmod
*
* @param string $path
* @param long $mode
* @param bool $recursive
*/
public final function chmod($path, $mode, $recursive = FALSE) {
if (!in_array('FileTransferChmodInterface', class_implements(get_class($this)))) {
throw new FileTransferException('Unable to change file permissions');
}
$path = $this->sanitizePath($path);
$path = $this->fixRemotePath($path);
$this->checkPath($path);
$this->chmodJailed($path, $mode, $recursive);
}
/**
* Creates a directory.
*
* @param $directory
* The directory to be created.
*/
public final function createDirectory($directory) {
$directory = $this->fixRemotePath($directory);
$this->checkPath($directory);
$this->createDirectoryJailed($directory);
}
/**
* Removes a directory.
*
* @param $directory
* The directory to be removed.
*/
public final function removeDirectory($directory) {
$directory = $this->fixRemotePath($directory);
$this->checkPath($directory);
$this->removeDirectoryJailed($directory);
}
/**
* Copies a file.
*
* @param $source
* The source file.
* @param $destination
* The destination file.
*/
public final function copyFile($source, $destination) {
$source = $this->sanitizePath($source);
$destination = $this->fixRemotePath($destination);
$this->checkPath($destination);
$this->copyFileJailed($source, $destination);
}
/**
* Removes a file.
*
* @param $destination
* The destination file to be removed.
*/
public final function removeFile($destination) {
$destination = $this->fixRemotePath($destination);
$this->checkPath($destination);
$this->removeFileJailed($destination);
}
/**
* Checks that the path is inside the jail and throws an exception if not.
*
* @param $path
* A path to check against the jail.
*/
protected final function checkPath($path) {
$full_jail = $this->chroot . $this->jail;
$full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
$full_path = $this->fixRemotePath($full_path, FALSE);
if ($full_jail !== $full_path) {
throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
}
}
/**
* Returns a modified path suitable for passing to the server.
* If a path is a windows path, makes it POSIX compliant by removing the drive letter.
* If $this->chroot has a value, it is stripped from the path to allow for
* chroot'd filetransfer systems.
*
* @param $path
* @param $strip_chroot
*
* @return string
*/
protected final function fixRemotePath($path, $strip_chroot = TRUE) {
$path = $this->sanitizePath($path);
$path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
if ($strip_chroot) {
if ($this->chroot && strpos($path, $this->chroot) === 0) {
$path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
}
}
return $path;
}
/**
* Changes backslashes to slashes, also removes a trailing slash.
*
* @param string $path
* @return string
*/
function sanitizePath($path) {
$path = str_replace('\\', '/', $path); // Windows path sanitization.
if (substr($path, -1) == '/') {
$path = substr($path, 0, -1);
}
return $path;
}
/**
* Copies a directory.
*
* We need a separate method to make the $destination is in the jail.
*
* @param $source
* The source path.
* @param $destination
* The destination path.
*/
protected function copyDirectoryJailed($source, $destination) {
if ($this->isDirectory($destination)) {
$destination = $destination . '/' . basename($source);
}
$this->createDirectory($destination);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
$relative_path = substr($filename, strlen($source));
if ($file->isDir()) {
$this->createDirectory($destination . $relative_path);
}
else {
$this->copyFile($file->getPathName(), $destination . $relative_path);
}
}
}
/**
* Creates a directory.
*
* @param $directory
* The directory to be created.
*/
abstract protected function createDirectoryJailed($directory);
/**
* Removes a directory.
*
* @param $directory
* The directory to be removed.
*/
abstract protected function removeDirectoryJailed($directory);
/**
* Copies a file.
*
* @param $source
* The source file.
* @param $destination
* The destination file.
*/
abstract protected function copyFileJailed($source, $destination);
/**
* Removes a file.
*
* @param $destination
* The destination file to be removed.
*/
abstract protected function removeFileJailed($destination);
/**
* Checks if a particular path is a directory
*
* @param $path
* The path to check
*
* @return boolean
*/
abstract public function isDirectory($path);
/**
* Checks if a particular path is a file (not a directory).
*
* @param $path
* The path to check
*
* @return boolean
*/
abstract public function isFile($path);
/**
* Return the chroot property for this connection.
*
* It does this by moving up the tree until it finds itself. If successful,
* it will return the chroot, otherwise FALSE.
*
* @return
* The chroot path for this connection or FALSE.
*/
function findChroot() {
// If the file exists as is, there is no chroot.
$path = __FILE__;
$path = $this->fixRemotePath($path, FALSE);
if ($this->isFile($path)) {
return FALSE;
}
$path = dirname(__FILE__);
$path = $this->fixRemotePath($path, FALSE);
$parts = explode('/', $path);
$chroot = '';
while (count($parts)) {
$check = implode($parts, '/');
if ($this->isFile($check . '/' . basename(__FILE__))) {
// Remove the trailing slash.
return substr($chroot,0,-1);
}
$chroot .= array_shift($parts) . '/';
}
return FALSE;
}
/**
* Sets the chroot and changes the jail to match the correct path scheme
*
*/
function setChroot() {
$this->chroot = $this->findChroot();
$this->jail = $this->fixRemotePath($this->jail);
}
}
/**
* FileTransferException class.
*/
class FileTransferException extends Exception {
public $arguments;
function __construct($message, $code = 0, $arguments = array()) {
parent::__construct($message, $code);
$this->arguments = $arguments;
}
}
/**
* A FileTransfer Class implementing this interface can be used to chmod files.
*/
interface FileTransferChmodInterface {
/**
* Changes the permissions of the file / directory specified in $path
*
* @param string $path
* Path to change permissions of.
* @param long $mode
* @see http://php.net/chmod
* @param boolean $recursive
* Pass TRUE to recursively chmod the entire directory specified in $path.
*/
function chmodJailed($path, $mode, $recursive);
}