1<?php 2 3namespace League\Flysystem\Adapter; 4 5use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait; 6use League\Flysystem\AdapterInterface; 7use League\Flysystem\Config; 8use League\Flysystem\ConnectionErrorException; 9use League\Flysystem\ConnectionRuntimeException; 10use League\Flysystem\InvalidRootException; 11use League\Flysystem\Util; 12use League\Flysystem\Util\MimeType; 13 14class Ftp extends AbstractFtpAdapter 15{ 16 use StreamedCopyTrait; 17 18 /** 19 * @var int 20 */ 21 protected $transferMode = FTP_BINARY; 22 23 /** 24 * @var null|bool 25 */ 26 protected $ignorePassiveAddress = null; 27 28 /** 29 * @var bool 30 */ 31 protected $recurseManually = false; 32 33 /** 34 * @var bool 35 */ 36 protected $utf8 = false; 37 38 /** 39 * @var array 40 */ 41 protected $configurable = [ 42 'host', 43 'port', 44 'username', 45 'password', 46 'ssl', 47 'timeout', 48 'root', 49 'permPrivate', 50 'permPublic', 51 'passive', 52 'transferMode', 53 'systemType', 54 'ignorePassiveAddress', 55 'recurseManually', 56 'utf8', 57 'enableTimestampsOnUnixListings', 58 ]; 59 60 /** 61 * @var bool 62 */ 63 protected $isPureFtpd; 64 65 /** 66 * Set the transfer mode. 67 * 68 * @param int $mode 69 * 70 * @return $this 71 */ 72 public function setTransferMode($mode) 73 { 74 $this->transferMode = $mode; 75 76 return $this; 77 } 78 79 /** 80 * Set if Ssl is enabled. 81 * 82 * @param bool $ssl 83 * 84 * @return $this 85 */ 86 public function setSsl($ssl) 87 { 88 $this->ssl = (bool) $ssl; 89 90 return $this; 91 } 92 93 /** 94 * Set if passive mode should be used. 95 * 96 * @param bool $passive 97 */ 98 public function setPassive($passive = true) 99 { 100 $this->passive = $passive; 101 } 102 103 /** 104 * @param bool $ignorePassiveAddress 105 */ 106 public function setIgnorePassiveAddress($ignorePassiveAddress) 107 { 108 $this->ignorePassiveAddress = $ignorePassiveAddress; 109 } 110 111 /** 112 * @param bool $recurseManually 113 */ 114 public function setRecurseManually($recurseManually) 115 { 116 $this->recurseManually = $recurseManually; 117 } 118 119 /** 120 * @param bool $utf8 121 */ 122 public function setUtf8($utf8) 123 { 124 $this->utf8 = (bool) $utf8; 125 } 126 127 /** 128 * Connect to the FTP server. 129 */ 130 public function connect() 131 { 132 $tries = 3; 133 start_connecting: 134 135 if ($this->ssl) { 136 $this->connection = @ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout()); 137 } else { 138 $this->connection = @ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout()); 139 } 140 141 if ( ! $this->connection) { 142 $tries--; 143 144 if ($tries > 0) goto start_connecting; 145 146 throw new ConnectionRuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort()); 147 } 148 149 $this->login(); 150 $this->setUtf8Mode(); 151 $this->setConnectionPassiveMode(); 152 $this->setConnectionRoot(); 153 $this->isPureFtpd = $this->isPureFtpdServer(); 154 } 155 156 /** 157 * Set the connection to UTF-8 mode. 158 */ 159 protected function setUtf8Mode() 160 { 161 if ($this->utf8) { 162 $response = ftp_raw($this->connection, "OPTS UTF8 ON"); 163 if (substr($response[0], 0, 3) !== '200') { 164 throw new ConnectionRuntimeException( 165 'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort() 166 ); 167 } 168 } 169 } 170 171 /** 172 * Set the connections to passive mode. 173 * 174 * @throws ConnectionRuntimeException 175 */ 176 protected function setConnectionPassiveMode() 177 { 178 if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) { 179 ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress); 180 } 181 182 if ( ! ftp_pasv($this->connection, $this->passive)) { 183 throw new ConnectionRuntimeException( 184 'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort() 185 ); 186 } 187 } 188 189 /** 190 * Set the connection root. 191 */ 192 protected function setConnectionRoot() 193 { 194 $root = $this->getRoot(); 195 $connection = $this->connection; 196 197 if ($root && ! ftp_chdir($connection, $root)) { 198 throw new InvalidRootException('Root is invalid or does not exist: ' . $this->getRoot()); 199 } 200 201 // Store absolute path for further reference. 202 // This is needed when creating directories and 203 // initial root was a relative path, else the root 204 // would be relative to the chdir'd path. 205 $this->root = ftp_pwd($connection); 206 } 207 208 /** 209 * Login. 210 * 211 * @throws ConnectionRuntimeException 212 */ 213 protected function login() 214 { 215 set_error_handler(function () { 216 }); 217 $isLoggedIn = ftp_login( 218 $this->connection, 219 $this->getUsername(), 220 $this->getPassword() 221 ); 222 restore_error_handler(); 223 224 if ( ! $isLoggedIn) { 225 $this->disconnect(); 226 throw new ConnectionRuntimeException( 227 'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort( 228 ) . ', username: ' . $this->getUsername() 229 ); 230 } 231 } 232 233 /** 234 * Disconnect from the FTP server. 235 */ 236 public function disconnect() 237 { 238 if (is_resource($this->connection)) { 239 @ftp_close($this->connection); 240 } 241 242 $this->connection = null; 243 } 244 245 /** 246 * @inheritdoc 247 */ 248 public function write($path, $contents, Config $config) 249 { 250 $stream = fopen('php://temp', 'w+b'); 251 fwrite($stream, $contents); 252 rewind($stream); 253 $result = $this->writeStream($path, $stream, $config); 254 fclose($stream); 255 256 if ($result === false) { 257 return false; 258 } 259 260 $result['contents'] = $contents; 261 $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents); 262 263 return $result; 264 } 265 266 /** 267 * @inheritdoc 268 */ 269 public function writeStream($path, $resource, Config $config) 270 { 271 $this->ensureDirectory(Util::dirname($path)); 272 273 if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) { 274 return false; 275 } 276 277 if ($visibility = $config->get('visibility')) { 278 $this->setVisibility($path, $visibility); 279 } 280 281 $type = 'file'; 282 283 return compact('type', 'path', 'visibility'); 284 } 285 286 /** 287 * @inheritdoc 288 */ 289 public function update($path, $contents, Config $config) 290 { 291 return $this->write($path, $contents, $config); 292 } 293 294 /** 295 * @inheritdoc 296 */ 297 public function updateStream($path, $resource, Config $config) 298 { 299 return $this->writeStream($path, $resource, $config); 300 } 301 302 /** 303 * @inheritdoc 304 */ 305 public function rename($path, $newpath) 306 { 307 return ftp_rename($this->getConnection(), $path, $newpath); 308 } 309 310 /** 311 * @inheritdoc 312 */ 313 public function delete($path) 314 { 315 return ftp_delete($this->getConnection(), $path); 316 } 317 318 /** 319 * @inheritdoc 320 */ 321 public function deleteDir($dirname) 322 { 323 $connection = $this->getConnection(); 324 $contents = array_reverse($this->listDirectoryContents($dirname, false)); 325 326 foreach ($contents as $object) { 327 if ($object['type'] === 'file') { 328 if ( ! ftp_delete($connection, $object['path'])) { 329 return false; 330 } 331 } elseif ( ! $this->deleteDir($object['path'])) { 332 return false; 333 } 334 } 335 336 return ftp_rmdir($connection, $dirname); 337 } 338 339 /** 340 * @inheritdoc 341 */ 342 public function createDir($dirname, Config $config) 343 { 344 $connection = $this->getConnection(); 345 $directories = explode('/', $dirname); 346 347 foreach ($directories as $directory) { 348 if (false === $this->createActualDirectory($directory, $connection)) { 349 $this->setConnectionRoot(); 350 351 return false; 352 } 353 354 ftp_chdir($connection, $directory); 355 } 356 357 $this->setConnectionRoot(); 358 359 return ['type' => 'dir', 'path' => $dirname]; 360 } 361 362 /** 363 * Create a directory. 364 * 365 * @param string $directory 366 * @param resource $connection 367 * 368 * @return bool 369 */ 370 protected function createActualDirectory($directory, $connection) 371 { 372 // List the current directory 373 $listing = ftp_nlist($connection, '.') ?: []; 374 375 foreach ($listing as $key => $item) { 376 if (preg_match('~^\./.*~', $item)) { 377 $listing[$key] = substr($item, 2); 378 } 379 } 380 381 if (in_array($directory, $listing, true)) { 382 return true; 383 } 384 385 return (boolean) ftp_mkdir($connection, $directory); 386 } 387 388 /** 389 * @inheritdoc 390 */ 391 public function getMetadata($path) 392 { 393 if ($path === '') { 394 return ['type' => 'dir', 'path' => '']; 395 } 396 397 if (@ftp_chdir($this->getConnection(), $path) === true) { 398 $this->setConnectionRoot(); 399 400 return ['type' => 'dir', 'path' => $path]; 401 } 402 403 $listing = $this->ftpRawlist('-A', $path); 404 405 if (empty($listing) || in_array('total 0', $listing, true)) { 406 return false; 407 } 408 409 if (preg_match('/.* not found/', $listing[0])) { 410 return false; 411 } 412 413 if (preg_match('/^total [0-9]*$/', $listing[0])) { 414 array_shift($listing); 415 } 416 417 return $this->normalizeObject($listing[0], ''); 418 } 419 420 /** 421 * @inheritdoc 422 */ 423 public function getMimetype($path) 424 { 425 if ( ! $metadata = $this->getMetadata($path)) { 426 return false; 427 } 428 429 $metadata['mimetype'] = MimeType::detectByFilename($path); 430 431 return $metadata; 432 } 433 434 /** 435 * @inheritdoc 436 */ 437 public function getTimestamp($path) 438 { 439 $timestamp = ftp_mdtm($this->getConnection(), $path); 440 441 return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false; 442 } 443 444 /** 445 * @inheritdoc 446 */ 447 public function read($path) 448 { 449 if ( ! $object = $this->readStream($path)) { 450 return false; 451 } 452 453 $object['contents'] = stream_get_contents($object['stream']); 454 fclose($object['stream']); 455 unset($object['stream']); 456 457 return $object; 458 } 459 460 /** 461 * @inheritdoc 462 */ 463 public function readStream($path) 464 { 465 $stream = fopen('php://temp', 'w+b'); 466 $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode); 467 rewind($stream); 468 469 if ( ! $result) { 470 fclose($stream); 471 472 return false; 473 } 474 475 return ['type' => 'file', 'path' => $path, 'stream' => $stream]; 476 } 477 478 /** 479 * @inheritdoc 480 */ 481 public function setVisibility($path, $visibility) 482 { 483 $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate(); 484 485 if ( ! ftp_chmod($this->getConnection(), $mode, $path)) { 486 return false; 487 } 488 489 return compact('path', 'visibility'); 490 } 491 492 /** 493 * @inheritdoc 494 * 495 * @param string $directory 496 */ 497 protected function listDirectoryContents($directory, $recursive = true) 498 { 499 if ($recursive && $this->recurseManually) { 500 return $this->listDirectoryContentsRecursive($directory); 501 } 502 503 $options = $recursive ? '-alnR' : '-aln'; 504 $listing = $this->ftpRawlist($options, $directory); 505 506 return $listing ? $this->normalizeListing($listing, $directory) : []; 507 } 508 509 /** 510 * @inheritdoc 511 * 512 * @param string $directory 513 */ 514 protected function listDirectoryContentsRecursive($directory) 515 { 516 $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory); 517 $output = []; 518 519 foreach ($listing as $item) { 520 $output[] = $item; 521 if ($item['type'] !== 'dir') { 522 continue; 523 } 524 $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path'])); 525 } 526 527 return $output; 528 } 529 530 /** 531 * Check if the connection is open. 532 * 533 * @return bool 534 * 535 * @throws ConnectionErrorException 536 */ 537 public function isConnected() 538 { 539 return is_resource($this->connection) 540 && $this->getRawExecResponseCode('NOOP') === 200; 541 } 542 543 /** 544 * @return bool 545 */ 546 protected function isPureFtpdServer() 547 { 548 $response = ftp_raw($this->connection, 'HELP'); 549 550 return stripos(implode(' ', $response), 'Pure-FTPd') !== false; 551 } 552 553 /** 554 * The ftp_rawlist function with optional escaping. 555 * 556 * @param string $options 557 * @param string $path 558 * 559 * @return array 560 */ 561 protected function ftpRawlist($options, $path) 562 { 563 $connection = $this->getConnection(); 564 565 if ($this->isPureFtpd) { 566 $path = str_replace(' ', '\ ', $path); 567 $this->escapePath($path); 568 } 569 570 return ftp_rawlist($connection, $options . ' ' . $path); 571 } 572 573 private function getRawExecResponseCode($command) 574 { 575 $response = @ftp_raw($this->connection, trim($command)); 576 577 return (int) preg_replace('/\D/', '', implode(' ', $response)); 578 } 579} 580