Ftp.php 14 KB

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