Ftp.php 14 KB

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