annotate core/lib/Drupal/Core/FileTransfer/FileTransfer.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
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@14 210 $full_path = \Drupal::service('file_system')
Chris@14 211 ->realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
Chris@0 212 $full_path = $this->fixRemotePath($full_path, FALSE);
Chris@0 213 if ($full_jail !== $full_path) {
Chris@0 214 throw new FileTransferException('@directory is outside of the @jail', NULL, ['@directory' => $path, '@jail' => $this->jail]);
Chris@0 215 }
Chris@0 216 }
Chris@0 217
Chris@0 218 /**
Chris@0 219 * Returns a modified path suitable for passing to the server.
Chris@0 220 *
Chris@0 221 * If a path is a windows path, makes it POSIX compliant by removing the drive
Chris@0 222 * letter. If $this->chroot has a value and $strip_chroot is TRUE, it is
Chris@0 223 * stripped from the path to allow for chroot'd filetransfer systems.
Chris@0 224 *
Chris@0 225 * @param string $path
Chris@0 226 * The path to modify.
Chris@0 227 * @param bool $strip_chroot
Chris@0 228 * Whether to remove the path in $this->chroot.
Chris@0 229 *
Chris@0 230 * @return string
Chris@0 231 * The modified path.
Chris@0 232 */
Chris@0 233 final protected function fixRemotePath($path, $strip_chroot = TRUE) {
Chris@0 234 $path = $this->sanitizePath($path);
Chris@0 235 // Strip out windows driveletter if its there.
Chris@0 236 $path = preg_replace('|^([a-z]{1}):|i', '', $path);
Chris@0 237 if ($strip_chroot) {
Chris@0 238 if ($this->chroot && strpos($path, $this->chroot) === 0) {
Chris@0 239 $path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
Chris@0 240 }
Chris@0 241 }
Chris@0 242 return $path;
Chris@0 243 }
Chris@0 244
Chris@0 245 /**
Chris@0 246 * Changes backslashes to slashes, also removes a trailing slash.
Chris@0 247 *
Chris@0 248 * @param string $path
Chris@0 249 * The path to modify.
Chris@0 250 *
Chris@0 251 * @return string
Chris@0 252 * The modified path.
Chris@0 253 */
Chris@0 254 public function sanitizePath($path) {
Chris@0 255 // Windows path sanitization.
Chris@0 256 $path = str_replace('\\', '/', $path);
Chris@0 257 if (substr($path, -1) == '/') {
Chris@0 258 $path = substr($path, 0, -1);
Chris@0 259 }
Chris@0 260 return $path;
Chris@0 261 }
Chris@0 262
Chris@0 263 /**
Chris@0 264 * Copies a directory.
Chris@0 265 *
Chris@0 266 * We need a separate method to make sure the $destination is in the jail.
Chris@0 267 *
Chris@0 268 * @param string $source
Chris@0 269 * The source path.
Chris@0 270 * @param string $destination
Chris@0 271 * The destination path.
Chris@0 272 */
Chris@0 273 protected function copyDirectoryJailed($source, $destination) {
Chris@0 274 if ($this->isDirectory($destination)) {
Chris@18 275 $destination = $destination . '/' . \Drupal::service('file_system')->basename($source);
Chris@0 276 }
Chris@0 277 $this->createDirectory($destination);
Chris@0 278 foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
Chris@0 279 $relative_path = substr($filename, strlen($source));
Chris@0 280 if ($file->isDir()) {
Chris@0 281 $this->createDirectory($destination . $relative_path);
Chris@0 282 }
Chris@0 283 else {
Chris@0 284 $this->copyFile($file->getPathName(), $destination . $relative_path);
Chris@0 285 }
Chris@0 286 }
Chris@0 287 }
Chris@0 288
Chris@0 289 /**
Chris@0 290 * Creates a directory.
Chris@0 291 *
Chris@0 292 * @param string $directory
Chris@0 293 * The directory to be created.
Chris@0 294 */
Chris@0 295 abstract protected function createDirectoryJailed($directory);
Chris@0 296
Chris@0 297 /**
Chris@0 298 * Removes a directory.
Chris@0 299 *
Chris@0 300 * @param string $directory
Chris@0 301 * The directory to be removed.
Chris@0 302 */
Chris@0 303 abstract protected function removeDirectoryJailed($directory);
Chris@0 304
Chris@0 305 /**
Chris@0 306 * Copies a file.
Chris@0 307 *
Chris@0 308 * @param string $source
Chris@0 309 * The source file.
Chris@0 310 * @param string $destination
Chris@0 311 * The destination file.
Chris@0 312 */
Chris@0 313 abstract protected function copyFileJailed($source, $destination);
Chris@0 314
Chris@0 315 /**
Chris@0 316 * Removes a file.
Chris@0 317 *
Chris@0 318 * @param string $destination
Chris@0 319 * The destination file to be removed.
Chris@0 320 */
Chris@0 321 abstract protected function removeFileJailed($destination);
Chris@0 322
Chris@0 323 /**
Chris@0 324 * Checks if a particular path is a directory.
Chris@0 325 *
Chris@0 326 * @param string $path
Chris@0 327 * The path to check
Chris@0 328 *
Chris@0 329 * @return bool
Chris@0 330 * TRUE if the specified path is a directory, FALSE otherwise.
Chris@0 331 */
Chris@0 332 abstract public function isDirectory($path);
Chris@0 333
Chris@0 334 /**
Chris@0 335 * Checks if a particular path is a file (not a directory).
Chris@0 336 *
Chris@0 337 * @param string $path
Chris@0 338 * The path to check.
Chris@0 339 *
Chris@0 340 * @return bool
Chris@0 341 * TRUE if the specified path is a file, FALSE otherwise.
Chris@0 342 */
Chris@0 343 abstract public function isFile($path);
Chris@0 344
Chris@0 345 /**
Chris@0 346 * Returns the chroot property for this connection.
Chris@0 347 *
Chris@0 348 * It does this by moving up the tree until it finds itself
Chris@0 349 *
Chris@0 350 * @return string|bool
Chris@0 351 * If successful, the chroot path for this connection, otherwise FALSE.
Chris@0 352 */
Chris@0 353 public function findChroot() {
Chris@0 354 // If the file exists as is, there is no chroot.
Chris@0 355 $path = __FILE__;
Chris@0 356 $path = $this->fixRemotePath($path, FALSE);
Chris@0 357 if ($this->isFile($path)) {
Chris@0 358 return FALSE;
Chris@0 359 }
Chris@0 360
Chris@0 361 $path = __DIR__;
Chris@0 362 $path = $this->fixRemotePath($path, FALSE);
Chris@0 363 $parts = explode('/', $path);
Chris@0 364 $chroot = '';
Chris@0 365 while (count($parts)) {
Chris@0 366 $check = implode($parts, '/');
Chris@18 367 if ($this->isFile($check . '/' . \Drupal::service('file_system')->basename(__FILE__))) {
Chris@0 368 // Remove the trailing slash.
Chris@0 369 return substr($chroot, 0, -1);
Chris@0 370 }
Chris@0 371 $chroot .= array_shift($parts) . '/';
Chris@0 372 }
Chris@0 373 return FALSE;
Chris@0 374 }
Chris@0 375
Chris@0 376 /**
Chris@0 377 * Sets the chroot and changes the jail to match the correct path scheme.
Chris@0 378 */
Chris@0 379 public function setChroot() {
Chris@0 380 $this->chroot = $this->findChroot();
Chris@0 381 $this->jail = $this->fixRemotePath($this->jail);
Chris@0 382 }
Chris@0 383
Chris@0 384 /**
Chris@0 385 * Returns a form to collect connection settings credentials.
Chris@0 386 *
Chris@0 387 * Implementing classes can either extend this form with fields collecting the
Chris@0 388 * specific information they need, or override it entirely.
Chris@0 389 *
Chris@0 390 * @return array
Chris@0 391 * An array that contains a Form API definition.
Chris@0 392 */
Chris@0 393 public function getSettingsForm() {
Chris@0 394 $form['username'] = [
Chris@0 395 '#type' => 'textfield',
Chris@0 396 '#title' => t('Username'),
Chris@0 397 ];
Chris@0 398 $form['password'] = [
Chris@0 399 '#type' => 'password',
Chris@0 400 '#title' => t('Password'),
Chris@0 401 '#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
Chris@0 402 ];
Chris@0 403 $form['advanced'] = [
Chris@0 404 '#type' => 'details',
Chris@0 405 '#title' => t('Advanced settings'),
Chris@0 406 ];
Chris@0 407 $form['advanced']['hostname'] = [
Chris@0 408 '#type' => 'textfield',
Chris@0 409 '#title' => t('Host'),
Chris@0 410 '#default_value' => 'localhost',
Chris@0 411 '#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 412 ];
Chris@0 413 $form['advanced']['port'] = [
Chris@0 414 '#type' => 'textfield',
Chris@0 415 '#title' => t('Port'),
Chris@0 416 '#default_value' => NULL,
Chris@0 417 ];
Chris@0 418 return $form;
Chris@0 419 }
Chris@0 420
Chris@0 421 }