FtpConnection.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
  5. *
  6. * @license GNU AGPL version 3 or any later version
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OCA\Files_External\Lib\Storage;
  23. /**
  24. * Low level wrapper around the ftp functions that smooths over some difference between servers
  25. */
  26. class FtpConnection {
  27. /** @var resource|\FTP\Connection */
  28. private $connection;
  29. public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) {
  30. if ($secure) {
  31. $connection = ftp_ssl_connect($hostname, $port);
  32. } else {
  33. $connection = ftp_connect($hostname, $port);
  34. }
  35. if ($connection === false) {
  36. throw new \Exception("Failed to connect to ftp");
  37. }
  38. if (ftp_login($connection, $username, $password) === false) {
  39. throw new \Exception("Failed to connect to login to ftp");
  40. }
  41. ftp_pasv($connection, true);
  42. $this->connection = $connection;
  43. }
  44. public function __destruct() {
  45. if ($this->connection) {
  46. ftp_close($this->connection);
  47. }
  48. $this->connection = null;
  49. }
  50. public function setUtf8Mode(): bool {
  51. $response = ftp_raw($this->connection, "OPTS UTF8 ON");
  52. return substr($response[0], 0, 3) === '200';
  53. }
  54. public function fput(string $path, $handle) {
  55. return @ftp_fput($this->connection, $path, $handle, FTP_BINARY);
  56. }
  57. public function fget($handle, string $path) {
  58. return @ftp_fget($this->connection, $handle, $path, FTP_BINARY);
  59. }
  60. public function mkdir(string $path) {
  61. return @ftp_mkdir($this->connection, $path);
  62. }
  63. public function chdir(string $path) {
  64. return @ftp_chdir($this->connection, $path);
  65. }
  66. public function delete(string $path) {
  67. return @ftp_delete($this->connection, $path);
  68. }
  69. public function rmdir(string $path) {
  70. return @ftp_rmdir($this->connection, $path);
  71. }
  72. public function rename(string $source, string $target) {
  73. return @ftp_rename($this->connection, $source, $target);
  74. }
  75. public function mdtm(string $path): int {
  76. $result = @ftp_mdtm($this->connection, $path);
  77. // filezilla doesn't like empty path with mdtm
  78. if ($result === -1 && $path === "") {
  79. $result = @ftp_mdtm($this->connection, "/");
  80. }
  81. return $result;
  82. }
  83. public function size(string $path) {
  84. return @ftp_size($this->connection, $path);
  85. }
  86. public function systype() {
  87. return @ftp_systype($this->connection);
  88. }
  89. public function nlist(string $path) {
  90. $files = @ftp_nlist($this->connection, $path);
  91. return array_map(function ($name) {
  92. if (strpos($name, '/') !== false) {
  93. $name = basename($name);
  94. }
  95. return $name;
  96. }, $files);
  97. }
  98. public function mlsd(string $path) {
  99. $files = @ftp_mlsd($this->connection, $path);
  100. if ($files !== false) {
  101. return array_map(function ($file) {
  102. if (strpos($file['name'], '/') !== false) {
  103. $file['name'] = basename($file['name']);
  104. }
  105. return $file;
  106. }, $files);
  107. } else {
  108. // not all servers support mlsd, in those cases we parse the raw list ourselves
  109. $rawList = @ftp_rawlist($this->connection, '-aln ' . $path);
  110. if ($rawList === false) {
  111. return false;
  112. }
  113. return $this->parseRawList($rawList, $path);
  114. }
  115. }
  116. // rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem
  117. private function parseRawList(array $rawList, string $directory): array {
  118. return array_map(function ($item) use ($directory) {
  119. return $this->parseRawListItem($item, $directory);
  120. }, $rawList);
  121. }
  122. private function parseRawListItem(string $item, string $directory): array {
  123. $isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item);
  124. return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory);
  125. }
  126. private function parseUnixItem(string $item, string $directory): array {
  127. $item = preg_replace('#\s+#', ' ', $item, 7);
  128. if (count(explode(' ', $item, 9)) !== 9) {
  129. throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
  130. }
  131. [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9);
  132. if ($name === '.') {
  133. $type = 'cdir';
  134. } elseif ($name === '..') {
  135. $type = 'pdir';
  136. } else {
  137. $type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
  138. }
  139. $parsedDate = (new \DateTime())
  140. ->setTimestamp(strtotime("$month $day $time"));
  141. $tomorrow = (new \DateTime())->add(new \DateInterval("P1D"));
  142. // since the provided date doesn't include the year, we either set it to the correct year
  143. // or when the date would otherwise be in the future (by more then 1 day to account for timezone errors)
  144. // we use last year
  145. if ($parsedDate > $tomorrow) {
  146. $parsedDate = $parsedDate->sub(new \DateInterval("P1Y"));
  147. }
  148. $formattedDate = $parsedDate
  149. ->format('YmdHis');
  150. return [
  151. 'type' => $type,
  152. 'name' => $name,
  153. 'modify' => $formattedDate,
  154. 'perm' => $this->normalizePermissions($permissions),
  155. 'size' => (int)$size,
  156. ];
  157. }
  158. private function normalizePermissions(string $permissions) {
  159. $isDir = substr($permissions, 0, 1) === 'd';
  160. // remove the type identifier and only use owner permissions
  161. $permissions = substr($permissions, 1, 4);
  162. // map the string rights to the ftp counterparts
  163. $filePermissionsMap = ['r' => 'r', 'w' => 'fadfw'];
  164. $dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp'];
  165. $map = $isDir ? $dirPermissionsMap : $filePermissionsMap;
  166. return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) {
  167. if (isset($map[$permission])) {
  168. $ftpPermissions .= $map[$permission];
  169. }
  170. return $ftpPermissions;
  171. }, '');
  172. }
  173. private function parseWindowsItem(string $item, string $directory): array {
  174. $item = preg_replace('#\s+#', ' ', trim($item), 3);
  175. if (count(explode(' ', $item, 4)) !== 4) {
  176. throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
  177. }
  178. [$date, $time, $size, $name] = explode(' ', $item, 4);
  179. // Check for the correct date/time format
  180. $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
  181. $formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis');
  182. if ($name === '.') {
  183. $type = 'cdir';
  184. } elseif ($name === '..') {
  185. $type = 'pdir';
  186. } else {
  187. $type = ($size === '<DIR>') ? 'dir' : 'file';
  188. }
  189. return [
  190. 'type' => $type,
  191. 'name' => $name,
  192. 'modify' => $formattedDate,
  193. 'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe',
  194. 'size' => (int)$size,
  195. ];
  196. }
  197. }