Ftp.php 14 KB

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