safeStorage = new SafeStorage(); $this->setConfig($config); } /** * Set the config. * * @param array $config * * @return $this */ public function setConfig(array $config) { foreach ($this->configurable as $setting) { if ( ! isset($config[$setting])) { continue; } $method = 'set' . ucfirst($setting); if (method_exists($this, $method)) { $this->$method($config[$setting]); } } return $this; } /** * Returns the host. * * @return string */ public function getHost() { return $this->host; } /** * Set the host. * * @param string $host * * @return $this */ public function setHost($host) { $this->host = $host; return $this; } /** * Set the public permission value. * * @param int $permPublic * * @return $this */ public function setPermPublic($permPublic) { $this->permPublic = $permPublic; return $this; } /** * Set the private permission value. * * @param int $permPrivate * * @return $this */ public function setPermPrivate($permPrivate) { $this->permPrivate = $permPrivate; return $this; } /** * Returns the ftp port. * * @return int */ public function getPort() { return $this->port; } /** * Returns the root folder to work from. * * @return string */ public function getRoot() { return $this->root; } /** * Set the ftp port. * * @param int|string $port * * @return $this */ public function setPort($port) { $this->port = (int) $port; return $this; } /** * Set the root folder to work from. * * @param string $root * * @return $this */ public function setRoot($root) { $this->root = rtrim($root, '\\/') . $this->separator; return $this; } /** * Returns the ftp username. * * @return string username */ public function getUsername() { $username = $this->safeStorage->retrieveSafely('username'); return $username !== null ? $username : 'anonymous'; } /** * Set ftp username. * * @param string $username * * @return $this */ public function setUsername($username) { $this->safeStorage->storeSafely('username', $username); return $this; } /** * Returns the password. * * @return string password */ public function getPassword() { return $this->safeStorage->retrieveSafely('password'); } /** * Set the ftp password. * * @param string $password * * @return $this */ public function setPassword($password) { $this->safeStorage->storeSafely('password', $password); return $this; } /** * Returns the amount of seconds before the connection will timeout. * * @return int */ public function getTimeout() { return $this->timeout; } /** * Set the amount of seconds before the connection should timeout. * * @param int $timeout * * @return $this */ public function setTimeout($timeout) { $this->timeout = (int) $timeout; return $this; } /** * Return the FTP system type. * * @return string */ public function getSystemType() { return $this->systemType; } /** * Set the FTP system type (windows or unix). * * @param string $systemType * * @return $this */ public function setSystemType($systemType) { $this->systemType = strtolower($systemType); return $this; } /** * True to enable timestamps for FTP servers that return unix-style listings. * * @param bool $bool * * @return $this */ public function setEnableTimestampsOnUnixListings($bool = false) { $this->enableTimestampsOnUnixListings = $bool; return $this; } /** * @inheritdoc */ public function listContents($directory = '', $recursive = false) { return $this->listDirectoryContents($directory, $recursive); } abstract protected function listDirectoryContents($directory, $recursive = false); /** * Normalize a directory listing. * * @param array $listing * @param string $prefix * * @return array directory listing */ protected function normalizeListing(array $listing, $prefix = '') { $base = $prefix; $result = []; $listing = $this->removeDotDirectories($listing); while ($item = array_shift($listing)) { if (preg_match('#^.*:$#', $item)) { $base = preg_replace('~^\./*|:$~', '', $item); continue; } $result[] = $this->normalizeObject($item, $base); } return $this->sortListing($result); } /** * Sort a directory listing. * * @param array $result * * @return array sorted listing */ protected function sortListing(array $result) { $compare = function ($one, $two) { return strnatcmp($one['path'], $two['path']); }; usort($result, $compare); return $result; } /** * Normalize a file entry. * * @param string $item * @param string $base * * @return array normalized file array * * @throws NotSupportedException */ protected function normalizeObject($item, $base) { $systemType = $this->systemType ?: $this->detectSystemType($item); if ($systemType === 'unix') { return $this->normalizeUnixObject($item, $base); } elseif ($systemType === 'windows') { return $this->normalizeWindowsObject($item, $base); } throw NotSupportedException::forFtpSystemType($systemType); } /** * Normalize a Unix file entry. * * Given $item contains: * '-rw-r--r-- 1 ftp ftp 409 Aug 19 09:01 file1.txt' * * This function will return: * [ * 'type' => 'file', * 'path' => 'file1.txt', * 'visibility' => 'public', * 'size' => 409, * 'timestamp' => 1566205260 * ] * * @param string $item * @param string $base * * @return array normalized file array */ protected function normalizeUnixObject($item, $base) { $item = preg_replace('#\s+#', ' ', trim($item), 7); if (count(explode(' ', $item, 9)) !== 9) { throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); } list($permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name) = explode(' ', $item, 9); $type = $this->detectType($permissions); $path = $base === '' ? $name : $base . $this->separator . $name; if ($type === 'dir') { $result = compact('type', 'path'); if ($this->enableTimestampsOnUnixListings) { $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear); $result += compact('timestamp'); } return $result; } $permissions = $this->normalizePermissions($permissions); $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE; $size = (int) $size; $result = compact('type', 'path', 'visibility', 'size'); if ($this->enableTimestampsOnUnixListings) { $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear); $result += compact('timestamp'); } return $result; } /** * Only accurate to the minute (current year), or to the day. * * Inadequacies in timestamp accuracy are due to limitations of the FTP 'LIST' command * * Note: The 'MLSD' command is a machine-readable replacement for 'LIST' * but many FTP servers do not support it :( * * @param string $month e.g. 'Aug' * @param string $day e.g. '19' * @param string $timeOrYear e.g. '09:01' OR '2015' * * @return int */ protected function normalizeUnixTimestamp($month, $day, $timeOrYear) { if (is_numeric($timeOrYear)) { $year = $timeOrYear; $hour = '00'; $minute = '00'; $seconds = '00'; } else { $year = date('Y'); list($hour, $minute) = explode(':', $timeOrYear); $seconds = '00'; } $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}"); return $dateTime->getTimestamp(); } /** * Normalize a Windows/DOS file entry. * * @param string $item * @param string $base * * @return array normalized file array */ protected function normalizeWindowsObject($item, $base) { $item = preg_replace('#\s+#', ' ', trim($item), 3); if (count(explode(' ', $item, 4)) !== 4) { throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); } list($date, $time, $size, $name) = explode(' ', $item, 4); $path = $base === '' ? $name : $base . $this->separator . $name; // Check for the correct date/time format $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; $dt = DateTime::createFromFormat($format, $date . $time); $timestamp = $dt ? $dt->getTimestamp() : (int) strtotime("$date $time"); if ($size === '') { $type = 'dir'; return compact('type', 'path', 'timestamp'); } $type = 'file'; $visibility = AdapterInterface::VISIBILITY_PUBLIC; $size = (int) $size; return compact('type', 'path', 'visibility', 'size', 'timestamp'); } /** * Get the system type from a listing item. * * @param string $item * * @return string the system type */ protected function detectSystemType($item) { return preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item) ? 'windows' : 'unix'; } /** * Get the file type from the permissions. * * @param string $permissions * * @return string file type */ protected function detectType($permissions) { return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; } /** * Normalize a permissions string. * * @param string $permissions * * @return int */ protected function normalizePermissions($permissions) { if (is_numeric($permissions)) { return ((int) $permissions) & 0777; } // remove the type identifier $permissions = substr($permissions, 1); // map the string rights to the numeric counterparts $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1']; $permissions = strtr($permissions, $map); // split up the permission groups $parts = str_split($permissions, 3); // convert the groups $mapper = function ($part) { return array_sum(str_split($part)); }; // converts to decimal number return octdec(implode('', array_map($mapper, $parts))); } /** * Filter out dot-directories. * * @param array $list * * @return array */ public function removeDotDirectories(array $list) { $filter = function ($line) { return $line !== '' && ! preg_match('#.* \.(\.)?$|^total#', $line); }; return array_filter($list, $filter); } /** * @inheritdoc */ public function has($path) { return $this->getMetadata($path); } /** * @inheritdoc */ public function getSize($path) { return $this->getMetadata($path); } /** * @inheritdoc */ public function getVisibility($path) { return $this->getMetadata($path); } /** * Ensure a directory exists. * * @param string $dirname */ public function ensureDirectory($dirname) { $dirname = (string) $dirname; if ($dirname !== '' && ! $this->has($dirname)) { $this->createDir($dirname, new Config()); } } /** * @return mixed */ public function getConnection() { if ( ! $this->isConnected()) { $this->disconnect(); $this->connect(); } return $this->connection; } /** * Get the public permission value. * * @return int */ public function getPermPublic() { return $this->permPublic; } /** * Get the private permission value. * * @return int */ public function getPermPrivate() { return $this->permPrivate; } /** * Disconnect on destruction. */ public function __destruct() { $this->disconnect(); } /** * Establish a connection. */ abstract public function connect(); /** * Close the connection. */ abstract public function disconnect(); /** * Check if a connection is active. * * @return bool */ abstract public function isConnected(); protected function escapePath($path) { return str_replace(['*', '[', ']'], ['\\*', '\\[', '\\]'], $path); } }