AbstractFtpAdapter.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. <?php
  2. namespace League\Flysystem\Adapter;
  3. use DateTime;
  4. use League\Flysystem\AdapterInterface;
  5. use League\Flysystem\Config;
  6. use League\Flysystem\NotSupportedException;
  7. use League\Flysystem\SafeStorage;
  8. use RuntimeException;
  9. abstract class AbstractFtpAdapter extends AbstractAdapter
  10. {
  11. /**
  12. * @var mixed
  13. */
  14. protected $connection;
  15. /**
  16. * @var string
  17. */
  18. protected $host;
  19. /**
  20. * @var int
  21. */
  22. protected $port = 21;
  23. /**
  24. * @var bool
  25. */
  26. protected $ssl = false;
  27. /**
  28. * @var int
  29. */
  30. protected $timeout = 90;
  31. /**
  32. * @var bool
  33. */
  34. protected $passive = true;
  35. /**
  36. * @var string
  37. */
  38. protected $separator = '/';
  39. /**
  40. * @var string|null
  41. */
  42. protected $root;
  43. /**
  44. * @var int
  45. */
  46. protected $permPublic = 0744;
  47. /**
  48. * @var int
  49. */
  50. protected $permPrivate = 0700;
  51. /**
  52. * @var array
  53. */
  54. protected $configurable = [];
  55. /**
  56. * @var string
  57. */
  58. protected $systemType;
  59. /**
  60. * @var SafeStorage
  61. */
  62. protected $safeStorage;
  63. /**
  64. * True to enable timestamps for FTP servers that return unix-style listings.
  65. *
  66. * @var bool
  67. */
  68. protected $enableTimestampsOnUnixListings = false;
  69. /**
  70. * Constructor.
  71. *
  72. * @param array $config
  73. */
  74. public function __construct(array $config)
  75. {
  76. $this->safeStorage = new SafeStorage();
  77. $this->setConfig($config);
  78. }
  79. /**
  80. * Set the config.
  81. *
  82. * @param array $config
  83. *
  84. * @return $this
  85. */
  86. public function setConfig(array $config)
  87. {
  88. foreach ($this->configurable as $setting) {
  89. if ( ! isset($config[$setting])) {
  90. continue;
  91. }
  92. $method = 'set' . ucfirst($setting);
  93. if (method_exists($this, $method)) {
  94. $this->$method($config[$setting]);
  95. }
  96. }
  97. return $this;
  98. }
  99. /**
  100. * Returns the host.
  101. *
  102. * @return string
  103. */
  104. public function getHost()
  105. {
  106. return $this->host;
  107. }
  108. /**
  109. * Set the host.
  110. *
  111. * @param string $host
  112. *
  113. * @return $this
  114. */
  115. public function setHost($host)
  116. {
  117. $this->host = $host;
  118. return $this;
  119. }
  120. /**
  121. * Set the public permission value.
  122. *
  123. * @param int $permPublic
  124. *
  125. * @return $this
  126. */
  127. public function setPermPublic($permPublic)
  128. {
  129. $this->permPublic = $permPublic;
  130. return $this;
  131. }
  132. /**
  133. * Set the private permission value.
  134. *
  135. * @param int $permPrivate
  136. *
  137. * @return $this
  138. */
  139. public function setPermPrivate($permPrivate)
  140. {
  141. $this->permPrivate = $permPrivate;
  142. return $this;
  143. }
  144. /**
  145. * Returns the ftp port.
  146. *
  147. * @return int
  148. */
  149. public function getPort()
  150. {
  151. return $this->port;
  152. }
  153. /**
  154. * Returns the root folder to work from.
  155. *
  156. * @return string
  157. */
  158. public function getRoot()
  159. {
  160. return $this->root;
  161. }
  162. /**
  163. * Set the ftp port.
  164. *
  165. * @param int|string $port
  166. *
  167. * @return $this
  168. */
  169. public function setPort($port)
  170. {
  171. $this->port = (int) $port;
  172. return $this;
  173. }
  174. /**
  175. * Set the root folder to work from.
  176. *
  177. * @param string $root
  178. *
  179. * @return $this
  180. */
  181. public function setRoot($root)
  182. {
  183. $this->root = rtrim($root, '\\/') . $this->separator;
  184. return $this;
  185. }
  186. /**
  187. * Returns the ftp username.
  188. *
  189. * @return string username
  190. */
  191. public function getUsername()
  192. {
  193. $username = $this->safeStorage->retrieveSafely('username');
  194. return $username !== null ? $username : 'anonymous';
  195. }
  196. /**
  197. * Set ftp username.
  198. *
  199. * @param string $username
  200. *
  201. * @return $this
  202. */
  203. public function setUsername($username)
  204. {
  205. $this->safeStorage->storeSafely('username', $username);
  206. return $this;
  207. }
  208. /**
  209. * Returns the password.
  210. *
  211. * @return string password
  212. */
  213. public function getPassword()
  214. {
  215. return $this->safeStorage->retrieveSafely('password');
  216. }
  217. /**
  218. * Set the ftp password.
  219. *
  220. * @param string $password
  221. *
  222. * @return $this
  223. */
  224. public function setPassword($password)
  225. {
  226. $this->safeStorage->storeSafely('password', $password);
  227. return $this;
  228. }
  229. /**
  230. * Returns the amount of seconds before the connection will timeout.
  231. *
  232. * @return int
  233. */
  234. public function getTimeout()
  235. {
  236. return $this->timeout;
  237. }
  238. /**
  239. * Set the amount of seconds before the connection should timeout.
  240. *
  241. * @param int $timeout
  242. *
  243. * @return $this
  244. */
  245. public function setTimeout($timeout)
  246. {
  247. $this->timeout = (int) $timeout;
  248. return $this;
  249. }
  250. /**
  251. * Return the FTP system type.
  252. *
  253. * @return string
  254. */
  255. public function getSystemType()
  256. {
  257. return $this->systemType;
  258. }
  259. /**
  260. * Set the FTP system type (windows or unix).
  261. *
  262. * @param string $systemType
  263. *
  264. * @return $this
  265. */
  266. public function setSystemType($systemType)
  267. {
  268. $this->systemType = strtolower($systemType);
  269. return $this;
  270. }
  271. /**
  272. * True to enable timestamps for FTP servers that return unix-style listings.
  273. *
  274. * @param bool $bool
  275. *
  276. * @return $this
  277. */
  278. public function setEnableTimestampsOnUnixListings($bool = false)
  279. {
  280. $this->enableTimestampsOnUnixListings = $bool;
  281. return $this;
  282. }
  283. /**
  284. * @inheritdoc
  285. */
  286. public function listContents($directory = '', $recursive = false)
  287. {
  288. return $this->listDirectoryContents($directory, $recursive);
  289. }
  290. abstract protected function listDirectoryContents($directory, $recursive = false);
  291. /**
  292. * Normalize a directory listing.
  293. *
  294. * @param array $listing
  295. * @param string $prefix
  296. *
  297. * @return array directory listing
  298. */
  299. protected function normalizeListing(array $listing, $prefix = '')
  300. {
  301. $base = $prefix;
  302. $result = [];
  303. $listing = $this->removeDotDirectories($listing);
  304. while ($item = array_shift($listing)) {
  305. if (preg_match('#^.*:$#', $item)) {
  306. $base = preg_replace('~^\./*|:$~', '', $item);
  307. continue;
  308. }
  309. $result[] = $this->normalizeObject($item, $base);
  310. }
  311. return $this->sortListing($result);
  312. }
  313. /**
  314. * Sort a directory listing.
  315. *
  316. * @param array $result
  317. *
  318. * @return array sorted listing
  319. */
  320. protected function sortListing(array $result)
  321. {
  322. $compare = function ($one, $two) {
  323. return strnatcmp($one['path'], $two['path']);
  324. };
  325. usort($result, $compare);
  326. return $result;
  327. }
  328. /**
  329. * Normalize a file entry.
  330. *
  331. * @param string $item
  332. * @param string $base
  333. *
  334. * @return array normalized file array
  335. *
  336. * @throws NotSupportedException
  337. */
  338. protected function normalizeObject($item, $base)
  339. {
  340. $systemType = $this->systemType ?: $this->detectSystemType($item);
  341. if ($systemType === 'unix') {
  342. return $this->normalizeUnixObject($item, $base);
  343. } elseif ($systemType === 'windows') {
  344. return $this->normalizeWindowsObject($item, $base);
  345. }
  346. throw NotSupportedException::forFtpSystemType($systemType);
  347. }
  348. /**
  349. * Normalize a Unix file entry.
  350. *
  351. * Given $item contains:
  352. * '-rw-r--r-- 1 ftp ftp 409 Aug 19 09:01 file1.txt'
  353. *
  354. * This function will return:
  355. * [
  356. * 'type' => 'file',
  357. * 'path' => 'file1.txt',
  358. * 'visibility' => 'public',
  359. * 'size' => 409,
  360. * 'timestamp' => 1566205260
  361. * ]
  362. *
  363. * @param string $item
  364. * @param string $base
  365. *
  366. * @return array normalized file array
  367. */
  368. protected function normalizeUnixObject($item, $base)
  369. {
  370. $item = preg_replace('#\s+#', ' ', trim($item), 7);
  371. if (count(explode(' ', $item, 9)) !== 9) {
  372. throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
  373. }
  374. list($permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name) = explode(' ', $item, 9);
  375. $type = $this->detectType($permissions);
  376. $path = $base === '' ? $name : $base . $this->separator . $name;
  377. if ($type === 'dir') {
  378. $result = compact('type', 'path');
  379. if ($this->enableTimestampsOnUnixListings) {
  380. $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear);
  381. $result += compact('timestamp');
  382. }
  383. return $result;
  384. }
  385. $permissions = $this->normalizePermissions($permissions);
  386. $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
  387. $size = (int) $size;
  388. $result = compact('type', 'path', 'visibility', 'size');
  389. if ($this->enableTimestampsOnUnixListings) {
  390. $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear);
  391. $result += compact('timestamp');
  392. }
  393. return $result;
  394. }
  395. /**
  396. * Only accurate to the minute (current year), or to the day.
  397. *
  398. * Inadequacies in timestamp accuracy are due to limitations of the FTP 'LIST' command
  399. *
  400. * Note: The 'MLSD' command is a machine-readable replacement for 'LIST'
  401. * but many FTP servers do not support it :(
  402. *
  403. * @param string $month e.g. 'Aug'
  404. * @param string $day e.g. '19'
  405. * @param string $timeOrYear e.g. '09:01' OR '2015'
  406. *
  407. * @return int
  408. */
  409. protected function normalizeUnixTimestamp($month, $day, $timeOrYear)
  410. {
  411. if (is_numeric($timeOrYear)) {
  412. $year = $timeOrYear;
  413. $hour = '00';
  414. $minute = '00';
  415. $seconds = '00';
  416. } else {
  417. $year = date('Y');
  418. list($hour, $minute) = explode(':', $timeOrYear);
  419. $seconds = '00';
  420. }
  421. $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}");
  422. return $dateTime->getTimestamp();
  423. }
  424. /**
  425. * Normalize a Windows/DOS file entry.
  426. *
  427. * @param string $item
  428. * @param string $base
  429. *
  430. * @return array normalized file array
  431. */
  432. protected function normalizeWindowsObject($item, $base)
  433. {
  434. $item = preg_replace('#\s+#', ' ', trim($item), 3);
  435. if (count(explode(' ', $item, 4)) !== 4) {
  436. throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
  437. }
  438. list($date, $time, $size, $name) = explode(' ', $item, 4);
  439. $path = $base === '' ? $name : $base . $this->separator . $name;
  440. // Check for the correct date/time format
  441. $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
  442. $dt = DateTime::createFromFormat($format, $date . $time);
  443. $timestamp = $dt ? $dt->getTimestamp() : (int) strtotime("$date $time");
  444. if ($size === '<DIR>') {
  445. $type = 'dir';
  446. return compact('type', 'path', 'timestamp');
  447. }
  448. $type = 'file';
  449. $visibility = AdapterInterface::VISIBILITY_PUBLIC;
  450. $size = (int) $size;
  451. return compact('type', 'path', 'visibility', 'size', 'timestamp');
  452. }
  453. /**
  454. * Get the system type from a listing item.
  455. *
  456. * @param string $item
  457. *
  458. * @return string the system type
  459. */
  460. protected function detectSystemType($item)
  461. {
  462. return preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', trim($item)) ? 'windows' : 'unix';
  463. }
  464. /**
  465. * Get the file type from the permissions.
  466. *
  467. * @param string $permissions
  468. *
  469. * @return string file type
  470. */
  471. protected function detectType($permissions)
  472. {
  473. return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
  474. }
  475. /**
  476. * Normalize a permissions string.
  477. *
  478. * @param string $permissions
  479. *
  480. * @return int
  481. */
  482. protected function normalizePermissions($permissions)
  483. {
  484. if (is_numeric($permissions)) {
  485. return ((int) $permissions) & 0777;
  486. }
  487. // remove the type identifier
  488. $permissions = substr($permissions, 1);
  489. // map the string rights to the numeric counterparts
  490. $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
  491. $permissions = strtr($permissions, $map);
  492. // split up the permission groups
  493. $parts = str_split($permissions, 3);
  494. // convert the groups
  495. $mapper = function ($part) {
  496. return array_sum(str_split($part));
  497. };
  498. // converts to decimal number
  499. return octdec(implode('', array_map($mapper, $parts)));
  500. }
  501. /**
  502. * Filter out dot-directories.
  503. *
  504. * @param array $list
  505. *
  506. * @return array
  507. */
  508. public function removeDotDirectories(array $list)
  509. {
  510. $filter = function ($line) {
  511. return $line !== '' && ! preg_match('#.* \.(\.)?$|^total#', $line);
  512. };
  513. return array_filter($list, $filter);
  514. }
  515. /**
  516. * @inheritdoc
  517. */
  518. public function has($path)
  519. {
  520. return $this->getMetadata($path);
  521. }
  522. /**
  523. * @inheritdoc
  524. */
  525. public function getSize($path)
  526. {
  527. return $this->getMetadata($path);
  528. }
  529. /**
  530. * @inheritdoc
  531. */
  532. public function getVisibility($path)
  533. {
  534. return $this->getMetadata($path);
  535. }
  536. /**
  537. * Ensure a directory exists.
  538. *
  539. * @param string $dirname
  540. */
  541. public function ensureDirectory($dirname)
  542. {
  543. $dirname = (string) $dirname;
  544. if ($dirname !== '' && ! $this->has($dirname)) {
  545. $this->createDir($dirname, new Config());
  546. }
  547. }
  548. /**
  549. * @return mixed
  550. */
  551. public function getConnection()
  552. {
  553. if ( ! $this->isConnected()) {
  554. $this->disconnect();
  555. $this->connect();
  556. }
  557. return $this->connection;
  558. }
  559. /**
  560. * Get the public permission value.
  561. *
  562. * @return int
  563. */
  564. public function getPermPublic()
  565. {
  566. return $this->permPublic;
  567. }
  568. /**
  569. * Get the private permission value.
  570. *
  571. * @return int
  572. */
  573. public function getPermPrivate()
  574. {
  575. return $this->permPrivate;
  576. }
  577. /**
  578. * Disconnect on destruction.
  579. */
  580. public function __destruct()
  581. {
  582. $this->disconnect();
  583. }
  584. /**
  585. * Establish a connection.
  586. */
  587. abstract public function connect();
  588. /**
  589. * Close the connection.
  590. */
  591. abstract public function disconnect();
  592. /**
  593. * Check if a connection is active.
  594. *
  595. * @return bool
  596. */
  597. abstract public function isConnected();
  598. protected function escapePath($path)
  599. {
  600. return str_replace(['*', '[', ']'], ['\\*', '\\[', '\\]'], $path);
  601. }
  602. }