annotate core/lib/Drupal/Core/FileTransfer/FileTransfer.php @ 9:1fc0ff908d1f

Add another data file
author Chris Cannam
date Mon, 05 Feb 2018 12:34:32 +0000
parents 4c8ae668cc8c
children 1fec387a4317
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\FileTransfer;
Chris@0 4
Chris@0 5 /**
Chris@0 6 * Defines the base FileTransfer class.
Chris@0 7 *
Chris@0 8 * Classes extending this class perform file operations on directories not
Chris@0 9 * writable by the webserver. To achieve this, the class should connect back
Chris@0 10 * to the server using some backend (for example FTP or SSH). To keep security,
Chris@0 11 * the password should always be asked from the user and never stored. For
Chris@0 12 * safety, all methods operate only inside a "jail", by default the Drupal root.
Chris@0 13 */
Chris@0 14 abstract class FileTransfer {
Chris@0 15
Chris@0 16 /**
Chris@0 17 * The username for this file transfer.
Chris@0 18 *
Chris@0 19 * @var string
Chris@0 20 */
Chris@0 21 protected $username;
Chris@0 22
Chris@0 23 /**
Chris@0 24 * The password for this file transfer.
Chris@0 25 *
Chris@0 26 * @var string
Chris@0 27 */
Chris@0 28 protected $password;
Chris@0 29
Chris@0 30 /**
Chris@0 31 * The hostname for this file transfer.
Chris@0 32 *
Chris@0 33 * @var string
Chris@0 34 */
Chris@0 35 protected $hostname = 'localhost';
Chris@0 36
Chris@0 37 /**
Chris@0 38 * The port for this file transfer.
Chris@0 39 *
Chris@0 40 * @var int
Chris@0 41 */
Chris@0 42 protected $port;
Chris@0 43
Chris@0 44 /**
Chris@0 45 * Constructs a Drupal\Core\FileTransfer\FileTransfer object.
Chris@0 46 *
Chris@0 47 * @param $jail
Chris@0 48 * The full path where all file operations performed by this object will
Chris@0 49 * be restricted to. This prevents the FileTransfer classes from being
Chris@0 50 * able to touch other parts of the filesystem.
Chris@0 51 */
Chris@0 52 public function __construct($jail) {
Chris@0 53 $this->jail = $jail;
Chris@0 54 }
Chris@0 55
Chris@0 56 /**
Chris@0 57 * Defines a factory method for this class.
Chris@0 58 *
Chris@0 59 * Classes that extend this class must override the factory() static method.
Chris@0 60 * They should return a new instance of the appropriate FileTransfer subclass.
Chris@0 61 *
Chris@0 62 * @param string $jail
Chris@0 63 * The full path where all file operations performed by this object will
Chris@0 64 * be restricted to. This prevents the FileTransfer classes from being
Chris@0 65 * able to touch other parts of the filesystem.
Chris@0 66 * @param array $settings
Chris@0 67 * An array of connection settings for the FileTransfer subclass. If the
Chris@0 68 * getSettingsForm() method uses any nested settings, the same structure
Chris@0 69 * will be assumed here.
Chris@0 70 *
Chris@0 71 * @return object
Chris@0 72 * New instance of the appropriate FileTransfer subclass.
Chris@0 73 *
Chris@0 74 * @throws \Drupal\Core\FileTransfer\FileTransferException
Chris@0 75 */
Chris@0 76 public static function factory($jail, $settings) {
Chris@0 77 throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
Chris@0 78 }
Chris@0 79
Chris@0 80 /**
Chris@0 81 * Implements the magic __get() method.
Chris@0 82 *
Chris@0 83 * If the connection isn't set to anything, this will call the connect()
Chris@0 84 * method and return the result; afterwards, the connection will be returned
Chris@0 85 * directly without using this method.
Chris@0 86 *
Chris@0 87 * @param string $name
Chris@0 88 * The name of the variable to return.
Chris@0 89 *
Chris@0 90 * @return string|bool
Chris@0 91 * The variable specified in $name.
Chris@0 92 */
Chris@0 93 public function __get($name) {
Chris@0 94 if ($name == 'connection') {
Chris@0 95 $this->connect();
Chris@0 96 return $this->connection;
Chris@0 97 }
Chris@0 98
Chris@0 99 if ($name == 'chroot') {
Chris@0 100 $this->setChroot();
Chris@0 101 return $this->chroot;
Chris@0 102 }
Chris@0 103 }
Chris@0 104
Chris@0 105 /**
Chris@0 106 * Connects to the server.
Chris@0 107 */
Chris@0 108 abstract public function connect();
Chris@0 109
Chris@0 110 /**
Chris@0 111 * Copies a directory.
Chris@0 112 *
Chris@0 113 * @param string $source
Chris@0 114 * The source path.
Chris@0 115 * @param string $destination
Chris@0 116 * The destination path.
Chris@0 117 */
Chris@0 118 final public function copyDirectory($source, $destination) {
Chris@0 119 $source = $this->sanitizePath($source);
Chris@0 120 $destination = $this->fixRemotePath($destination);
Chris@0 121 $this->checkPath($destination);
Chris@0 122 $this->copyDirectoryJailed($source, $destination);
Chris@0 123 }
Chris@0 124
Chris@0 125 /**
Chris@0 126 * Changes the permissions of the specified $path (file or directory).
Chris@0 127 *
Chris@0 128 * @param string $path
Chris@0 129 * The file / directory to change the permissions of.
Chris@0 130 * @param int $mode
Chris@0 131 * The new file permission mode to be passed to chmod().
Chris@0 132 * @param bool $recursive
Chris@0 133 * Pass TRUE to recursively chmod the entire directory specified in $path.
Chris@0 134 *
Chris@0 135 * @throws \Drupal\Core\FileTransfer\FileTransferException
Chris@0 136 *
Chris@0 137 * @see http://php.net/chmod
Chris@0 138 */
Chris@0 139 final public function chmod($path, $mode, $recursive = FALSE) {
Chris@0 140 if (!($this instanceof ChmodInterface)) {
Chris@0 141 throw new FileTransferException('Unable to change file permissions');
Chris@0 142 }
Chris@0 143 $path = $this->sanitizePath($path);
Chris@0 144 $path = $this->fixRemotePath($path);
Chris@0 145 $this->checkPath($path);
Chris@0 146 $this->chmodJailed($path, $mode, $recursive);
Chris@0 147 }
Chris@0 148
Chris@0 149 /**
Chris@0 150 * Creates a directory.
Chris@0 151 *
Chris@0 152 * @param string $directory
Chris@0 153 * The directory to be created.
Chris@0 154 */
Chris@0 155 final public function createDirectory($directory) {
Chris@0 156 $directory = $this->fixRemotePath($directory);
Chris@0 157 $this->checkPath($directory);
Chris@0 158 $this->createDirectoryJailed($directory);
Chris@0 159 }
Chris@0 160
Chris@0 161 /**
Chris@0 162 * Removes a directory.
Chris@0 163 *
Chris@0 164 * @param string $directory
Chris@0 165 * The directory to be removed.
Chris@0 166 */
Chris@0 167 final public function removeDirectory($directory) {
Chris@0 168 $directory = $this->fixRemotePath($directory);
Chris@0 169 $this->checkPath($directory);
Chris@0 170 $this->removeDirectoryJailed($directory);
Chris@0 171 }
Chris@0 172
Chris@0 173 /**
Chris@0 174 * Copies a file.
Chris@0 175 *
Chris@0 176 * @param string $source
Chris@0 177 * The source file.
Chris@0 178 * @param string $destination
Chris@0 179 * The destination file.
Chris@0 180 */
Chris@0 181 final public function copyFile($source, $destination) {
Chris@0 182 $source = $this->sanitizePath($source);
Chris@0 183 $destination = $this->fixRemotePath($destination);
Chris@0 184 $this->checkPath($destination);
Chris@0 185 $this->copyFileJailed($source, $destination);
Chris@0 186 }
Chris@0 187
Chris@0 188 /**
Chris@0 189 * Removes a file.
Chris@0 190 *
Chris@0 191 * @param string $destination
Chris@0 192 * The destination file to be removed.
Chris@0 193 */
Chris@0 194 final public function removeFile($destination) {
Chris@0 195 $destination = $this->fixRemotePath($destination);
Chris@0 196 $this->checkPath($destination);
Chris@0 197 $this->removeFileJailed($destination);
Chris@0 198 }
Chris@0 199
Chris@0 200 /**
Chris@0 201 * Checks that the path is inside the jail and throws an exception if not.
Chris@0 202 *
Chris@0 203 * @param string $path
Chris@0 204 * A path to check against the jail.
Chris@0 205 *
Chris@0 206 * @throws \Drupal\Core\FileTransfer\FileTransferException
Chris@0 207 */
Chris@0 208 final protected function checkPath($path) {
Chris@0 209 $full_jail = $this->chroot . $this->jail;
Chris@0 210 $full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
Chris@0 211 $full_path = $this->fixRemotePath($full_path, FALSE);
Chris@0 212 if ($full_jail !== $full_path) {
Chris@0 213 throw new FileTransferException('@directory is outside of the @jail', NULL, ['@directory' => $path, '@jail' => $this->jail]);
Chris@0 214 }
Chris@0 215 }
Chris@0 216
Chris@0 217 /**
Chris@0 218 * Returns a modified path suitable for passing to the server.
Chris@0 219 *
Chris@0 220 * If a path is a windows path, makes it POSIX compliant by removing the drive
Chris@0 221 * letter. If $this->chroot has a value and $strip_chroot is TRUE, it is
Chris@0 222 * stripped from the path to allow for chroot'd filetransfer systems.
Chris@0 223 *
Chris@0 224 * @param string $path
Chris@0 225 * The path to modify.
Chris@0 226 * @param bool $strip_chroot
Chris@0 227 * Whether to remove the path in $this->chroot.
Chris@0 228 *
Chris@0 229 * @return string
Chris@0 230 * The modified path.
Chris@0 231 */
Chris@0 232 final protected function fixRemotePath($path, $strip_chroot = TRUE) {
Chris@0 233 $path = $this->sanitizePath($path);
Chris@0 234 // Strip out windows driveletter if its there.
Chris@0 235 $path = preg_replace('|^([a-z]{1}):|i', '', $path);
Chris@0 236 if ($strip_chroot) {
Chris@0 237 if ($this->chroot && strpos($path, $this->chroot) === 0) {
Chris@0 238 $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
Chris@0 239 }
Chris@0 240 }
Chris@0 241 return $path;
Chris@0 242 }
Chris@0 243
Chris@0 244 /**
Chris@0 245 * Changes backslashes to slashes, also removes a trailing slash.
Chris@0 246 *
Chris@0 247 * @param string $path
Chris@0 248 * The path to modify.
Chris@0 249 *
Chris@0 250 * @return string
Chris@0 251 * The modified path.
Chris@0 252 */
Chris@0 253 public function sanitizePath($path) {
Chris@0 254 // Windows path sanitization.
Chris@0 255 $path = str_replace('\\', '/', $path);
Chris@0 256 if (substr($path, -1) == '/') {
Chris@0 257 $path = substr($path, 0, -1);
Chris@0 258 }
Chris@0 259 return $path;
Chris@0 260 }
Chris@0 261
Chris@0 262 /**
Chris@0 263 * Copies a directory.
Chris@0 264 *
Chris@0 265 * We need a separate method to make sure the $destination is in the jail.
Chris@0 266 *
Chris@0 267 * @param string $source
Chris@0 268 * The source path.
Chris@0 269 * @param string $destination
Chris@0 270 * The destination path.
Chris@0 271 */
Chris@0 272 protected function copyDirectoryJailed($source, $destination) {
Chris@0 273 if ($this->isDirectory($destination)) {
Chris@0 274 $destination = $destination . '/' . drupal_basename($source);
Chris@0 275 }
Chris@0 276 $this->createDirectory($destination);
Chris@0 277 foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
Chris@0 278 $relative_path = substr($filename, strlen($source));
Chris@0 279 if ($file->isDir()) {
Chris@0 280 $this->createDirectory($destination . $relative_path);
Chris@0 281 }
Chris@0 282 else {
Chris@0 283 $this->copyFile($file->getPathName(), $destination . $relative_path);
Chris@0 284 }
Chris@0 285 }
Chris@0 286 }
Chris@0 287
Chris@0 288 /**
Chris@0 289 * Creates a directory.
Chris@0 290 *
Chris@0 291 * @param string $directory
Chris@0 292 * The directory to be created.
Chris@0 293 */
Chris@0 294 abstract protected function createDirectoryJailed($directory);
Chris@0 295
Chris@0 296 /**
Chris@0 297 * Removes a directory.
Chris@0 298 *
Chris@0 299 * @param string $directory
Chris@0 300 * The directory to be removed.
Chris@0 301 */
Chris@0 302 abstract protected function removeDirectoryJailed($directory);
Chris@0 303
Chris@0 304 /**
Chris@0 305 * Copies a file.
Chris@0 306 *
Chris@0 307 * @param string $source
Chris@0 308 * The source file.
Chris@0 309 * @param string $destination
Chris@0 310 * The destination file.
Chris@0 311 */
Chris@0 312 abstract protected function copyFileJailed($source, $destination);
Chris@0 313
Chris@0 314 /**
Chris@0 315 * Removes a file.
Chris@0 316 *
Chris@0 317 * @param string $destination
Chris@0 318 * The destination file to be removed.
Chris@0 319 */
Chris@0 320 abstract protected function removeFileJailed($destination);
Chris@0 321
Chris@0 322 /**
Chris@0 323 * Checks if a particular path is a directory.
Chris@0 324 *
Chris@0 325 * @param string $path
Chris@0 326 * The path to check
Chris@0 327 *
Chris@0 328 * @return bool
Chris@0 329 * TRUE if the specified path is a directory, FALSE otherwise.
Chris@0 330 */
Chris@0 331 abstract public function isDirectory($path);
Chris@0 332
Chris@0 333 /**
Chris@0 334 * Checks if a particular path is a file (not a directory).
Chris@0 335 *
Chris@0 336 * @param string $path
Chris@0 337 * The path to check.
Chris@0 338 *
Chris@0 339 * @return bool
Chris@0 340 * TRUE if the specified path is a file, FALSE otherwise.
Chris@0 341 */
Chris@0 342 abstract public function isFile($path);
Chris@0 343
Chris@0 344 /**
Chris@0 345 * Returns the chroot property for this connection.
Chris@0 346 *
Chris@0 347 * It does this by moving up the tree until it finds itself
Chris@0 348 *
Chris@0 349 * @return string|bool
Chris@0 350 * If successful, the chroot path for this connection, otherwise FALSE.
Chris@0 351 */
Chris@0 352 public function findChroot() {
Chris@0 353 // If the file exists as is, there is no chroot.
Chris@0 354 $path = __FILE__;
Chris@0 355 $path = $this->fixRemotePath($path, FALSE);
Chris@0 356 if ($this->isFile($path)) {
Chris@0 357 return FALSE;
Chris@0 358 }
Chris@0 359
Chris@0 360 $path = __DIR__;
Chris@0 361 $path = $this->fixRemotePath($path, FALSE);
Chris@0 362 $parts = explode('/', $path);
Chris@0 363 $chroot = '';
Chris@0 364 while (count($parts)) {
Chris@0 365 $check = implode($parts, '/');
Chris@0 366 if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
Chris@0 367 // Remove the trailing slash.
Chris@0 368 return substr($chroot, 0, -1);
Chris@0 369 }
Chris@0 370 $chroot .= array_shift($parts) . '/';
Chris@0 371 }
Chris@0 372 return FALSE;
Chris@0 373 }
Chris@0 374
Chris@0 375 /**
Chris@0 376 * Sets the chroot and changes the jail to match the correct path scheme.
Chris@0 377 */
Chris@0 378 public function setChroot() {
Chris@0 379 $this->chroot = $this->findChroot();
Chris@0 380 $this->jail = $this->fixRemotePath($this->jail);
Chris@0 381 }
Chris@0 382
Chris@0 383 /**
Chris@0 384 * Returns a form to collect connection settings credentials.
Chris@0 385 *
Chris@0 386 * Implementing classes can either extend this form with fields collecting the
Chris@0 387 * specific information they need, or override it entirely.
Chris@0 388 *
Chris@0 389 * @return array
Chris@0 390 * An array that contains a Form API definition.
Chris@0 391 */
Chris@0 392 public function getSettingsForm() {
Chris@0 393 $form['username'] = [
Chris@0 394 '#type' => 'textfield',
Chris@0 395 '#title' => t('Username'),
Chris@0 396 ];
Chris@0 397 $form['password'] = [
Chris@0 398 '#type' => 'password',
Chris@0 399 '#title' => t('Password'),
Chris@0 400 '#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
Chris@0 401 ];
Chris@0 402 $form['advanced'] = [
Chris@0 403 '#type' => 'details',
Chris@0 404 '#title' => t('Advanced settings'),
Chris@0 405 ];
Chris@0 406 $form['advanced']['hostname'] = [
Chris@0 407 '#type' => 'textfield',
Chris@0 408 '#title' => t('Host'),
Chris@0 409 '#default_value' => 'localhost',
Chris@0 410 '#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
Chris@0 411 ];
Chris@0 412 $form['advanced']['port'] = [
Chris@0 413 '#type' => 'textfield',
Chris@0 414 '#title' => t('Port'),
Chris@0 415 '#default_value' => NULL,
Chris@0 416 ];
Chris@0 417 return $form;
Chris@0 418 }
Chris@0 419
Chris@0 420 }