transferMode = $mode; return $this; } /** * Set if Ssl is enabled. * * @param bool $ssl * * @return $this */ public function setSsl($ssl) { $this->ssl = (bool) $ssl; return $this; } /** * Set if passive mode should be used. * * @param bool $passive */ public function setPassive($passive = true) { $this->passive = $passive; } /** * @param bool $ignorePassiveAddress */ public function setIgnorePassiveAddress($ignorePassiveAddress) { $this->ignorePassiveAddress = $ignorePassiveAddress; } /** * @param bool $recurseManually */ public function setRecurseManually($recurseManually) { $this->recurseManually = $recurseManually; } /** * @param bool $utf8 */ public function setUtf8($utf8) { $this->utf8 = (bool) $utf8; } /** * Connect to the FTP server. */ public function connect() { $tries = 3; start_connecting: if ($this->ssl) { $this->connection = @ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout()); } else { $this->connection = @ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout()); } if ( ! $this->connection) { $tries--; if ($tries > 0) goto start_connecting; throw new ConnectionRuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort()); } $this->login(); $this->setUtf8Mode(); $this->setConnectionPassiveMode(); $this->setConnectionRoot(); $this->isPureFtpd = $this->isPureFtpdServer(); } /** * Set the connection to UTF-8 mode. */ protected function setUtf8Mode() { if ($this->utf8) { $response = ftp_raw($this->connection, "OPTS UTF8 ON"); if (!in_array(substr($response[0], 0, 3), ['200', '202'])) { throw new ConnectionRuntimeException( 'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort() ); } } } /** * Set the connections to passive mode. * * @throws ConnectionRuntimeException */ protected function setConnectionPassiveMode() { if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) { ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress); } if ( ! ftp_pasv($this->connection, $this->passive)) { throw new ConnectionRuntimeException( 'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort() ); } } /** * Set the connection root. */ protected function setConnectionRoot() { $root = $this->getRoot(); $connection = $this->connection; if ($root && ! ftp_chdir($connection, $root)) { throw new InvalidRootException('Root is invalid or does not exist: ' . $this->getRoot()); } // Store absolute path for further reference. // This is needed when creating directories and // initial root was a relative path, else the root // would be relative to the chdir'd path. $this->root = ftp_pwd($connection); } /** * Login. * * @throws ConnectionRuntimeException */ protected function login() { set_error_handler(function () { }); $isLoggedIn = ftp_login( $this->connection, $this->getUsername(), $this->getPassword() ); restore_error_handler(); if ( ! $isLoggedIn) { $this->disconnect(); throw new ConnectionRuntimeException( 'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort( ) . ', username: ' . $this->getUsername() ); } } /** * Disconnect from the FTP server. */ public function disconnect() { if ($this->hasFtpConnection()) { @ftp_close($this->connection); } $this->connection = null; } /** * @inheritdoc */ public function write($path, $contents, Config $config) { $stream = fopen('php://temp', 'w+b'); fwrite($stream, $contents); rewind($stream); $result = $this->writeStream($path, $stream, $config); fclose($stream); if ($result === false) { return false; } $result['contents'] = $contents; $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents); return $result; } /** * @inheritdoc */ public function writeStream($path, $resource, Config $config) { $this->ensureDirectory(Util::dirname($path)); if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) { return false; } if ($visibility = $config->get('visibility')) { $this->setVisibility($path, $visibility); } $type = 'file'; return compact('type', 'path', 'visibility'); } /** * @inheritdoc */ public function update($path, $contents, Config $config) { return $this->write($path, $contents, $config); } /** * @inheritdoc */ public function updateStream($path, $resource, Config $config) { return $this->writeStream($path, $resource, $config); } /** * @inheritdoc */ public function rename($path, $newpath) { return ftp_rename($this->getConnection(), $path, $newpath); } /** * @inheritdoc */ public function delete($path) { return ftp_delete($this->getConnection(), $path); } /** * @inheritdoc */ public function deleteDir($dirname) { $connection = $this->getConnection(); $contents = array_reverse($this->listDirectoryContents($dirname, false)); foreach ($contents as $object) { if ($object['type'] === 'file') { if ( ! ftp_delete($connection, $object['path'])) { return false; } } elseif ( ! $this->deleteDir($object['path'])) { return false; } } return ftp_rmdir($connection, $dirname); } /** * @inheritdoc */ public function createDir($dirname, Config $config) { $connection = $this->getConnection(); $directories = explode('/', $dirname); foreach ($directories as $directory) { if (false === $this->createActualDirectory($directory, $connection)) { $this->setConnectionRoot(); return false; } ftp_chdir($connection, $directory); } $this->setConnectionRoot(); return ['type' => 'dir', 'path' => $dirname]; } /** * Create a directory. * * @param string $directory * @param resource $connection * * @return bool */ protected function createActualDirectory($directory, $connection) { // List the current directory $listing = ftp_nlist($connection, '.') ?: []; foreach ($listing as $key => $item) { if (preg_match('~^\./.*~', $item)) { $listing[$key] = substr($item, 2); } } if (in_array($directory, $listing, true)) { return true; } return (boolean) ftp_mkdir($connection, $directory); } /** * @inheritdoc */ public function getMetadata($path) { if ($path === '') { return ['type' => 'dir', 'path' => '']; } if (@ftp_chdir($this->getConnection(), $path) === true) { $this->setConnectionRoot(); return ['type' => 'dir', 'path' => $path]; } $listing = $this->ftpRawlist('-A', $path); if (empty($listing) || in_array('total 0', $listing, true)) { return false; } if (preg_match('/.* not found/', $listing[0])) { return false; } if (preg_match('/^total [0-9]*$/', $listing[0])) { array_shift($listing); } return $this->normalizeObject($listing[0], ''); } /** * @inheritdoc */ public function getMimetype($path) { if ( ! $metadata = $this->getMetadata($path)) { return false; } $metadata['mimetype'] = MimeType::detectByFilename($path); return $metadata; } /** * @inheritdoc */ public function getTimestamp($path) { $timestamp = ftp_mdtm($this->getConnection(), $path); return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false; } /** * @inheritdoc */ public function read($path) { if ( ! $object = $this->readStream($path)) { return false; } $object['contents'] = stream_get_contents($object['stream']); fclose($object['stream']); unset($object['stream']); return $object; } /** * @inheritdoc */ public function readStream($path) { $stream = fopen('php://temp', 'w+b'); $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode); rewind($stream); if ( ! $result) { fclose($stream); return false; } return ['type' => 'file', 'path' => $path, 'stream' => $stream]; } /** * @inheritdoc */ public function setVisibility($path, $visibility) { $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate(); if ( ! ftp_chmod($this->getConnection(), $mode, $path)) { return false; } return compact('path', 'visibility'); } /** * @inheritdoc * * @param string $directory */ protected function listDirectoryContents($directory, $recursive = true) { if ($recursive && $this->recurseManually) { return $this->listDirectoryContentsRecursive($directory); } $options = $recursive ? '-alnR' : '-aln'; $listing = $this->ftpRawlist($options, $directory); return $listing ? $this->normalizeListing($listing, $directory) : []; } /** * @inheritdoc * * @param string $directory */ protected function listDirectoryContentsRecursive($directory) { $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory); $output = []; foreach ($listing as $item) { $output[] = $item; if ($item['type'] !== 'dir') { continue; } $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path'])); } return $output; } /** * Check if the connection is open. * * @return bool * * @throws ConnectionErrorException */ public function isConnected() { return $this->hasFtpConnection() && $this->getRawExecResponseCode('NOOP') === 200; } /** * @return bool */ protected function isPureFtpdServer() { $response = ftp_raw($this->connection, 'HELP'); return stripos(implode(' ', $response), 'Pure-FTPd') !== false; } /** * The ftp_rawlist function with optional escaping. * * @param string $options * @param string $path * * @return array */ protected function ftpRawlist($options, $path) { $connection = $this->getConnection(); if ($this->isPureFtpd) { $path = str_replace([' ', '[', ']'], ['\ ', '\\[', '\\]'], $path); } return ftp_rawlist($connection, $options . ' ' . $this->escapePath($path)); } private function getRawExecResponseCode($command) { $response = @ftp_raw($this->connection, trim($command)); return (int) preg_replace('/\D/', '', implode(' ', $response)); } private function hasFtpConnection(): bool { return is_resource($this->connection) || $this->connection instanceof \FTP\Connection; } }