PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Destination / Vendors / FTP / FTPClient.php
backup / src / JetBackup / Destination / Vendors / FTP Last commit date
.htaccess 1 year ago ChunkedDownload.php 1 year ago ChunkedUpload.php 1 year ago DirIterator.php 1 year ago FTP.php 1 year ago FTPClient.php 1 year ago index.html 1 year ago web.config 1 year ago
FTPClient.php
436 lines
1 <?php
2 /*
3 *
4 * JetBackup @ package
5 * Created By Idan Ben-Ezra
6 *
7 * Copyrights @ JetApps
8 * https://www.jetapps.com
9 *
10 **/
11 namespace JetBackup\Destination\Vendors\FTP;
12
13 use Exception;
14 use JetBackup\Destination\DestinationFile;
15 use JetBackup\Destination\Integration\DestinationFile as iDestinationFile;
16 use JetBackup\Exception\HttpRequestException;
17 use JetBackup\Exception\IOException;
18 use JetBackup\Filesystem\File;
19 use JetBackup\Web\File\FileChunk;
20 use JetBackup\Web\File\FileDownload;
21 use JetBackup\Web\File\FileException;
22 use JetBackup\Web\File\FileStream;
23 use JetBackup\Web\JetHttp;
24 use JetBackup\Wordpress\Wordpress;
25
26 defined( '__JETBACKUP__' ) or die( 'Restricted access' );
27
28 class FTPClient {
29
30 const SERVER_TYPE_PURE_FTPD = 'Pure-FTPd'; // ---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
31 const SERVER_TYPE_PRO_FTPD = 'ProFTPD'; // ---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
32 const SERVER_TYPE_VSFTPD = 'vsFTPd'; // (vsFTPd 3.0.2)
33 const SERVER_TYPE_UNKNOWN = '';
34
35 const SERVER_TYPES = [self::SERVER_TYPE_PURE_FTPD,self::SERVER_TYPE_PRO_FTPD,self::SERVER_TYPE_VSFTPD];
36
37 const CODE_TRUNCATED = 1000;
38
39 private string $_hostname;
40 private int $_port;
41 private string $_auth;
42 private bool $_passive;
43 private bool $_secure;
44 private int $_timeout;
45 private int $_prefer_ip;
46 private ?JetHttp $_http=null;
47 private array $_mkdir_dirs=[];
48 private bool $_ignore_self_signed;
49
50 /**
51 * @param string $hostname
52 * @param string $username
53 * @param string $password
54 * @param int $port
55 * @param bool $passivemode
56 * @param bool $securessl
57 * @param int $timeout
58 * @param int $prefer_ip
59 * @param bool $ignore_self_signed
60 */
61 public function __construct(string $hostname, string $username, string $password, int $port=21, bool $passivemode=false, bool $securessl=false, int $timeout=30, int $prefer_ip = 0, bool $ignore_self_signed = true) {
62 $this->_hostname = $hostname;
63 $this->_port = $port;
64 $this->_auth = $username.':'.$password;
65 $this->_passive = $passivemode;
66 $this->_secure = $securessl;
67 $this->_timeout = $timeout;
68 $this->_prefer_ip = $prefer_ip;
69 $this->_ignore_self_signed = $ignore_self_signed;
70 }
71
72 /**
73 * @param string $perms
74 *
75 * @return int
76 */
77 private static function _calcPermissions(string $perms):int {
78 $permissions = 0;
79
80 if($perms[0] == 's') $permissions |= 0140000;
81 if($perms[0] == 'l') $permissions |= 0120000;
82 if($perms[0] == '-') $permissions |= 0100000;
83 if($perms[0] == 'b') $permissions |= 0060000;
84 if($perms[0] == 'd') $permissions |= 0040000;
85 if($perms[0] == 'c') $permissions |= 0020000;
86 if($perms[0] == 'p') $permissions |= 0010000;
87
88 if($perms[1] != '-') $permissions |= 00400;
89 if($perms[2] != '-') $permissions |= 00200;
90 if($perms[3] == 'x' || $perms[3] == 's' || $perms[3] == 't') $permissions |= 00100;
91 if($perms[3] != '-' && $perms[3] != 'x') $permissions |= 04000;
92
93 if($perms[4] != '-') $permissions |= 00040;
94 if($perms[5] != '-') $permissions |= 00020;
95 if($perms[6] == 'x' || $perms[6] == 's' || $perms[6] == 't') $permissions |= 00010;
96 if($perms[6] != '-' && $perms[6] != 'x') $permissions |= 02000;
97
98 if($perms[7] != '-') $permissions |= 00004;
99 if($perms[8] != '-') $permissions |= 00002;
100 if($perms[9] == 'x' || $perms[9] == 's' || $perms[9] == 't') $permissions |= 00001;
101 if($perms[9] != '-' && $perms[9] != 'x') $permissions |= 01000;
102
103 return $permissions;
104 }
105
106 /**
107 * @param string $source
108 * @param string $destination
109 *
110 * @return void
111 * @throws FileException
112 * @throws Exception
113 */
114 public function upload(string $source, string $destination):void {
115
116 if(!file_exists($source) || !is_file($source)) throw new Exception("Source file not exists");
117
118 $this->_init();
119
120 try {
121 $file = new FileStream($source);
122 $this->_http
123 ->addOption(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_MULTICWD)
124 ->addOption(CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_RETRY)
125 ->upload($this->_getURL($destination), $file);
126 } catch(HttpRequestException $e) {
127 throw new Exception($e->getMessage(), $e->getCode());
128 }
129 }
130
131 public function uploadChunk(FileChunk $chunk, $destination):void {
132
133 $this->_init();
134
135 try {
136 $this->_http
137 ->addOption(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_MULTICWD)
138 ->addOption(CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_RETRY)
139 ->addOption(CURLOPT_APPEND, 1)
140 ->uploadChunk($this->_getURL($destination), $chunk);
141 } catch(HttpRequestException $e) {
142 throw new Exception($e->getMessage(), $e->getCode());
143 }
144 }
145
146 /**
147 * @param string $source
148 * @param string $destination
149 *
150 * @return void
151 * @throws Exception
152 */
153 public function download(string $source, string $destination, int $start=0, int $end=0):void {
154
155 $file = new File($destination);
156 if($file->exists() && $file->isDir()) $destination .= '/' . basename($source);
157
158 $file = new File(dirname($destination));
159 if(!$file->exists() || (!$file->isDir() || $file->isLink())) throw new Exception("Destination folder not found");
160
161 $this->_init();
162
163 try {
164 $fileDownload = new FileDownload($destination);
165 if($start || $end) $this->_http->addOption(CURLOPT_RANGE, $start . '-' . $end);
166 $this->_http->download($this->_getURL($source), $fileDownload);
167 } catch(HttpRequestException $e) {
168 throw new Exception($e->getMessage(), $e->getCode());
169 }
170 }
171
172 /**
173 * @param string $file
174 *
175 * @return void
176 * @throws Exception
177 */
178 public function delete(string $file):void {
179 $this->_quote("DELE $file");
180 }
181
182 /**
183 * @param string $url
184 *
185 * @return string
186 */
187 private function _getURL(string $url):string {
188 $url = preg_replace("/\/+/", "/", "/$url");
189 return "ftp://$this->_hostname$url";
190 }
191
192 public function getFileSize(string $file) {
193 $this->_init();
194
195 try {
196 $response = $this->_http
197 ->setReturnTransfer()
198 ->setMethod(JetHttp::METHOD_HEAD)
199 ->exec($this->_getURL($file));
200
201 return $response->getHeaders()->getHeader('content-length') ?: 0;
202 } catch(HttpRequestException $e) {
203 if($e->getCode() == 78 || $e->getCode() == 9) return false;
204 throw $e;
205 }
206
207 }
208
209 /**
210 * @param string $file
211 *
212 * @return bool
213 * @throws HttpRequestException
214 */
215 public function fileExists(string $file): bool {
216 $this->_init();
217
218 try {
219 $response = $this->_http
220 ->setReturnTransfer()
221 ->setMethod(JetHttp::METHOD_HEAD)
222 ->exec($this->_getURL($file));
223
224 // We find that in some FTP configurations, we are getting positive responses even when the file does not exist.
225 // For those false-positive responses, we have noticed that the response does not contain a 'content-length' header.
226 return $response->getHeaders()->getHeader('content-length') !== null;
227 } catch(HttpRequestException $e) {
228 if($e->getCode() == 78 || $e->getCode() == 9) return false;
229 throw $e;
230 }
231 }
232
233 /**
234 * @param string $directory
235 *
236 * @return void
237 * @throws Exception
238 */
239 public function mkdir(string $directory):void {
240 $directory = trim(preg_replace("#/+#", '/', $directory), '/');
241 $parts = explode('/', $directory);
242
243 $path = '/';
244 foreach($parts as $part) {
245
246 if(in_array($path . $part . "/", $this->_mkdir_dirs)) {
247 $path .= $part . '/';
248 continue;
249 }
250 $list = $this->listDir($path);
251
252 foreach($list as $item) {
253 if($item->getName() == $part) {
254 $path .= $part . '/';
255 $this->_mkdir_dirs[] = $path;
256 continue 2;
257 }
258 }
259
260 $this->_quote("MKD $path$part/");
261
262 /**
263 * Wrapping the chmod with try-catch as it's not always supported and might trigger error
264 * Not supported in:
265 * - Microsoft FTP & vsFTPd
266 * - ProFTPD Works if AllowChmod is enabled
267 */
268 try {
269 $this->_quote("SITE CHMOD 0700 $path$part/");
270 } catch (Exception $e) {
271 // todo - log events
272 }
273
274
275 $path .= $part . '/';
276 $this->_mkdir_dirs[] = $path;
277 }
278 }
279
280 /**
281 * @param string $directory
282 *
283 * @return void
284 * @throws Exception
285 */
286 public function rmdir(string $directory):void {
287 $this->_quote("RMD $directory");
288 }
289
290 /**
291 * @param string $directory
292 *
293 * @return void
294 * @throws Exception
295 */
296 public function chdir(string $directory):void {
297 $this->_quote("CWD $directory");
298 }
299
300 /**
301 * @param string $cmd
302 *
303 * @return void
304 * @throws Exception
305 */
306 private function _quote(string $cmd):void {
307 $this->_init();
308
309 try {
310 $this->_http
311 ->setReturnTransfer()
312 ->addOption(CURLOPT_QUOTE, [$cmd])
313 ->exec($this->_getURL('/'));
314 } catch(HttpRequestException $e) {
315 throw new Exception($e->getMessage(), $e->getCode());
316 }
317 }
318
319 /**
320 * @param string $dir
321 *
322 * @return iDestinationFile[]
323 * @throws HttpRequestException
324 * @throws Exception
325 */
326 public function listDir(string $dir): array {
327
328 $dir = !$dir? '/': preg_replace("#/+#", "/", "$dir/");
329
330 $this->_init();
331
332 $response = $this->_http
333 ->setReturnTransfer()
334 ->addOption(CURLOPT_CUSTOMREQUEST, 'LIST -al')
335 ->exec($this->_getURL($dir));
336
337 $headers = $response->getHeaders()->getHeader('ftp');
338
339 $server_type = self::SERVER_TYPE_UNKNOWN;
340
341 foreach($headers as $line) {
342
343 if(!$server_type) {
344 foreach(self::SERVER_TYPES as $type) {
345 if(Wordpress::strContains($line, $type)) {
346 $server_type = $type;
347 break;
348 }
349 }
350 }
351
352 if(Wordpress::strContains($line, "226 Output truncated"))
353 throw new Exception("The LIST command output is truncated by the server, please change the FTP service configuration to a allow higher number of files to be displayed. Server Message: ". substr($line, 6), self::CODE_TRUNCATED);
354 }
355
356 $output = [];
357
358 switch($server_type) {
359 case self::SERVER_TYPE_UNKNOWN:
360 case self::SERVER_TYPE_PRO_FTPD:
361 case self::SERVER_TYPE_PURE_FTPD:
362 case self::SERVER_TYPE_VSFTPD:
363
364 $line = strtok($response->getBody(), "\r\n");
365 while ($line !== false) {
366 $parts = explode(' ', preg_replace("/\s+/", ' ', $line));
367 $line = strtok("\r\n");
368
369 list($perms,,$owner,$group,$size,$month,$day,$year) = $parts;
370
371 $filename = $parts[count($parts)-1];
372 $link = '';
373 if($parts[count($parts)-2] == '->') {
374 $filename = $parts[count($parts)-3];
375 $link = $parts[count($parts)-1];
376 }
377
378 if($filename == '.' || $filename == '..') continue;
379
380 $file = DestinationFile::genFile([
381 'perms' => self::_calcPermissions($perms),
382 'size' => $size,
383 'mtime' => strtotime($month . ' ' . $day . ' ' . $year),
384 'user' => $owner,
385 'group' => $group,
386 'path' => $dir . $filename,
387 'link' => $link,
388 ]);
389
390 $output[] = $file;
391 }
392
393 break;
394 }
395
396 return $output;
397 }
398
399 /**
400 * @return void
401 */
402 private function _init():void {
403
404 if(!$this->_http) $this->_http = JetHttp::request();
405 $this->_http->reset();
406
407 $this->_http->addOption(CURLOPT_FTPPORT, $this->_passive ? null : '-')
408 ->setAuth(null, $this->_auth, CURLAUTH_BASIC)
409 ->setPort($this->_port)
410 ->setFollowLocation()
411 ->setConnectionTimeout($this->_timeout);
412
413 if($this->_secure) {
414 if(defined('CURL_SSLVERSION_MAX_TLSv1_2')) $this->_http->addOption(CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_TLSv1_2);
415 $this->_http->addOption(CURLOPT_USE_SSL, CURLUSESSL_ALL);
416
417 if($this->_ignore_self_signed) {
418 $this->_http->addOption(CURLOPT_USE_SSL, CURLUSESSL_ALL)
419 ->addOption(CURLOPT_SSL_VERIFYPEER, false) // Disable certificate verification
420 ->addOption(CURLOPT_SSL_VERIFYHOST, 0); // Disable hostname verification
421 }
422
423 }
424
425 if($this->_prefer_ip)
426 $this->_http->addOption(CURLOPT_IPRESOLVE, $this->_prefer_ip == 4? CURL_IPRESOLVE_V4: CURL_IPRESOLVE_V6);
427 }
428
429 /**
430 * @return void
431 */
432 public function close():void {
433 $this->_http = null;
434 }
435 }
436