1<?php
2/**
3 * WordPress FTP Sockets Filesystem.
4 *
5 * @package WordPress
6 * @subpackage Filesystem
7 */
8
9/**
10 * WordPress Filesystem Class for implementing FTP Sockets.
11 *
12 * @since 2.5.0
13 *
14 * @see WP_Filesystem_Base
15 */
16class WP_Filesystem_ftpsockets extends WP_Filesystem_Base {
17
18	/**
19	 * @since 2.5.0
20	 * @var ftp
21	 */
22	public $ftp;
23
24	/**
25	 * Constructor.
26	 *
27	 * @since 2.5.0
28	 *
29	 * @param array $opt
30	 */
31	public function __construct( $opt = '' ) {
32		$this->method = 'ftpsockets';
33		$this->errors = new WP_Error();
34
35		// Check if possible to use ftp functions.
36		if ( ! include_once ABSPATH . 'wp-admin/includes/class-ftp.php' ) {
37			return;
38		}
39
40		$this->ftp = new ftp();
41
42		if ( empty( $opt['port'] ) ) {
43			$this->options['port'] = 21;
44		} else {
45			$this->options['port'] = (int) $opt['port'];
46		}
47
48		if ( empty( $opt['hostname'] ) ) {
49			$this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) );
50		} else {
51			$this->options['hostname'] = $opt['hostname'];
52		}
53
54		// Check if the options provided are OK.
55		if ( empty( $opt['username'] ) ) {
56			$this->errors->add( 'empty_username', __( 'FTP username is required' ) );
57		} else {
58			$this->options['username'] = $opt['username'];
59		}
60
61		if ( empty( $opt['password'] ) ) {
62			$this->errors->add( 'empty_password', __( 'FTP password is required' ) );
63		} else {
64			$this->options['password'] = $opt['password'];
65		}
66	}
67
68	/**
69	 * Connects filesystem.
70	 *
71	 * @since 2.5.0
72	 *
73	 * @return bool True on success, false on failure.
74	 */
75	public function connect() {
76		if ( ! $this->ftp ) {
77			return false;
78		}
79
80		$this->ftp->setTimeout( FS_CONNECT_TIMEOUT );
81
82		if ( ! $this->ftp->SetServer( $this->options['hostname'], $this->options['port'] ) ) {
83			$this->errors->add(
84				'connect',
85				sprintf(
86					/* translators: %s: hostname:port */
87					__( 'Failed to connect to FTP Server %s' ),
88					$this->options['hostname'] . ':' . $this->options['port']
89				)
90			);
91
92			return false;
93		}
94
95		if ( ! $this->ftp->connect() ) {
96			$this->errors->add(
97				'connect',
98				sprintf(
99					/* translators: %s: hostname:port */
100					__( 'Failed to connect to FTP Server %s' ),
101					$this->options['hostname'] . ':' . $this->options['port']
102				)
103			);
104
105			return false;
106		}
107
108		if ( ! $this->ftp->login( $this->options['username'], $this->options['password'] ) ) {
109			$this->errors->add(
110				'auth',
111				sprintf(
112					/* translators: %s: Username. */
113					__( 'Username/Password incorrect for %s' ),
114					$this->options['username']
115				)
116			);
117
118			return false;
119		}
120
121		$this->ftp->SetType( FTP_BINARY );
122		$this->ftp->Passive( true );
123		$this->ftp->setTimeout( FS_TIMEOUT );
124
125		return true;
126	}
127
128	/**
129	 * Reads entire file into a string.
130	 *
131	 * @since 2.5.0
132	 *
133	 * @param string $file Name of the file to read.
134	 * @return string|false Read data on success, false if no temporary file could be opened,
135	 *                      or if the file couldn't be retrieved.
136	 */
137	public function get_contents( $file ) {
138		if ( ! $this->exists( $file ) ) {
139			return false;
140		}
141
142		$tempfile   = wp_tempnam( $file );
143		$temphandle = fopen( $tempfile, 'w+' );
144
145		if ( ! $temphandle ) {
146			unlink( $tempfile );
147			return false;
148		}
149
150		mbstring_binary_safe_encoding();
151
152		if ( ! $this->ftp->fget( $temphandle, $file ) ) {
153			fclose( $temphandle );
154			unlink( $tempfile );
155
156			reset_mbstring_encoding();
157
158			return ''; // Blank document. File does exist, it's just blank.
159		}
160
161		reset_mbstring_encoding();
162
163		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
164		$contents = '';
165
166		while ( ! feof( $temphandle ) ) {
167			$contents .= fread( $temphandle, 8 * KB_IN_BYTES );
168		}
169
170		fclose( $temphandle );
171		unlink( $tempfile );
172
173		return $contents;
174	}
175
176	/**
177	 * Reads entire file into an array.
178	 *
179	 * @since 2.5.0
180	 *
181	 * @param string $file Path to the file.
182	 * @return array|false File contents in an array on success, false on failure.
183	 */
184	public function get_contents_array( $file ) {
185		return explode( "\n", $this->get_contents( $file ) );
186	}
187
188	/**
189	 * Writes a string to a file.
190	 *
191	 * @since 2.5.0
192	 *
193	 * @param string    $file     Remote path to the file where to write the data.
194	 * @param string    $contents The data to write.
195	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
196	 *                            Default false.
197	 * @return bool True on success, false on failure.
198	 */
199	public function put_contents( $file, $contents, $mode = false ) {
200		$tempfile   = wp_tempnam( $file );
201		$temphandle = @fopen( $tempfile, 'w+' );
202
203		if ( ! $temphandle ) {
204			unlink( $tempfile );
205			return false;
206		}
207
208		// The FTP class uses string functions internally during file download/upload.
209		mbstring_binary_safe_encoding();
210
211		$bytes_written = fwrite( $temphandle, $contents );
212
213		if ( false === $bytes_written || strlen( $contents ) !== $bytes_written ) {
214			fclose( $temphandle );
215			unlink( $tempfile );
216
217			reset_mbstring_encoding();
218
219			return false;
220		}
221
222		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
223
224		$ret = $this->ftp->fput( $file, $temphandle );
225
226		reset_mbstring_encoding();
227
228		fclose( $temphandle );
229		unlink( $tempfile );
230
231		$this->chmod( $file, $mode );
232
233		return $ret;
234	}
235
236	/**
237	 * Gets the current working directory.
238	 *
239	 * @since 2.5.0
240	 *
241	 * @return string|false The current working directory on success, false on failure.
242	 */
243	public function cwd() {
244		$cwd = $this->ftp->pwd();
245
246		if ( $cwd ) {
247			$cwd = trailingslashit( $cwd );
248		}
249
250		return $cwd;
251	}
252
253	/**
254	 * Changes current directory.
255	 *
256	 * @since 2.5.0
257	 *
258	 * @param string $dir The new current directory.
259	 * @return bool True on success, false on failure.
260	 */
261	public function chdir( $dir ) {
262		return $this->ftp->chdir( $dir );
263	}
264
265	/**
266	 * Changes filesystem permissions.
267	 *
268	 * @since 2.5.0
269	 *
270	 * @param string    $file      Path to the file.
271	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
272	 *                             0755 for directories. Default false.
273	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
274	 *                             Default false.
275	 * @return bool True on success, false on failure.
276	 */
277	public function chmod( $file, $mode = false, $recursive = false ) {
278		if ( ! $mode ) {
279			if ( $this->is_file( $file ) ) {
280				$mode = FS_CHMOD_FILE;
281			} elseif ( $this->is_dir( $file ) ) {
282				$mode = FS_CHMOD_DIR;
283			} else {
284				return false;
285			}
286		}
287
288		// chmod any sub-objects if recursive.
289		if ( $recursive && $this->is_dir( $file ) ) {
290			$filelist = $this->dirlist( $file );
291
292			foreach ( (array) $filelist as $filename => $filemeta ) {
293				$this->chmod( $file . '/' . $filename, $mode, $recursive );
294			}
295		}
296
297		// chmod the file or directory.
298		return $this->ftp->chmod( $file, $mode );
299	}
300
301	/**
302	 * Gets the file owner.
303	 *
304	 * @since 2.5.0
305	 *
306	 * @param string $file Path to the file.
307	 * @return string|false Username of the owner on success, false on failure.
308	 */
309	public function owner( $file ) {
310		$dir = $this->dirlist( $file );
311
312		return $dir[ $file ]['owner'];
313	}
314
315	/**
316	 * Gets the permissions of the specified file or filepath in their octal format.
317	 *
318	 * @since 2.5.0
319	 *
320	 * @param string $file Path to the file.
321	 * @return string Mode of the file (the last 3 digits).
322	 */
323	public function getchmod( $file ) {
324		$dir = $this->dirlist( $file );
325
326		return $dir[ $file ]['permsn'];
327	}
328
329	/**
330	 * Gets the file's group.
331	 *
332	 * @since 2.5.0
333	 *
334	 * @param string $file Path to the file.
335	 * @return string|false The group on success, false on failure.
336	 */
337	public function group( $file ) {
338		$dir = $this->dirlist( $file );
339
340		return $dir[ $file ]['group'];
341	}
342
343	/**
344	 * Copies a file.
345	 *
346	 * @since 2.5.0
347	 *
348	 * @param string    $source      Path to the source file.
349	 * @param string    $destination Path to the destination file.
350	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
351	 *                               Default false.
352	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
353	 *                               0755 for dirs. Default false.
354	 * @return bool True on success, false on failure.
355	 */
356	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
357		if ( ! $overwrite && $this->exists( $destination ) ) {
358			return false;
359		}
360
361		$content = $this->get_contents( $source );
362
363		if ( false === $content ) {
364			return false;
365		}
366
367		return $this->put_contents( $destination, $content, $mode );
368	}
369
370	/**
371	 * Moves a file.
372	 *
373	 * @since 2.5.0
374	 *
375	 * @param string $source      Path to the source file.
376	 * @param string $destination Path to the destination file.
377	 * @param bool   $overwrite   Optional. Whether to overwrite the destination file if it exists.
378	 *                            Default false.
379	 * @return bool True on success, false on failure.
380	 */
381	public function move( $source, $destination, $overwrite = false ) {
382		return $this->ftp->rename( $source, $destination );
383	}
384
385	/**
386	 * Deletes a file or directory.
387	 *
388	 * @since 2.5.0
389	 *
390	 * @param string       $file      Path to the file or directory.
391	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
392	 *                                Default false.
393	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
394	 *                                Default false.
395	 * @return bool True on success, false on failure.
396	 */
397	public function delete( $file, $recursive = false, $type = false ) {
398		if ( empty( $file ) ) {
399			return false;
400		}
401
402		if ( 'f' === $type || $this->is_file( $file ) ) {
403			return $this->ftp->delete( $file );
404		}
405
406		if ( ! $recursive ) {
407			return $this->ftp->rmdir( $file );
408		}
409
410		return $this->ftp->mdel( $file );
411	}
412
413	/**
414	 * Checks if a file or directory exists.
415	 *
416	 * @since 2.5.0
417	 *
418	 * @param string $file Path to file or directory.
419	 * @return bool Whether $file exists or not.
420	 */
421	public function exists( $file ) {
422		$list = $this->ftp->nlist( $file );
423
424		if ( empty( $list ) && $this->is_dir( $file ) ) {
425			return true; // File is an empty directory.
426		}
427
428		return ! empty( $list ); // Empty list = no file, so invert.
429		// Return $this->ftp->is_exists($file); has issues with ABOR+426 responses on the ncFTPd server.
430	}
431
432	/**
433	 * Checks if resource is a file.
434	 *
435	 * @since 2.5.0
436	 *
437	 * @param string $file File path.
438	 * @return bool Whether $file is a file.
439	 */
440	public function is_file( $file ) {
441		if ( $this->is_dir( $file ) ) {
442			return false;
443		}
444
445		if ( $this->exists( $file ) ) {
446			return true;
447		}
448
449		return false;
450	}
451
452	/**
453	 * Checks if resource is a directory.
454	 *
455	 * @since 2.5.0
456	 *
457	 * @param string $path Directory path.
458	 * @return bool Whether $path is a directory.
459	 */
460	public function is_dir( $path ) {
461		$cwd = $this->cwd();
462
463		if ( $this->chdir( $path ) ) {
464			$this->chdir( $cwd );
465			return true;
466		}
467
468		return false;
469	}
470
471	/**
472	 * Checks if a file is readable.
473	 *
474	 * @since 2.5.0
475	 *
476	 * @param string $file Path to file.
477	 * @return bool Whether $file is readable.
478	 */
479	public function is_readable( $file ) {
480		return true;
481	}
482
483	/**
484	 * Checks if a file or directory is writable.
485	 *
486	 * @since 2.5.0
487	 *
488	 * @param string $file Path to file or directory.
489	 * @return bool Whether $file is writable.
490	 */
491	public function is_writable( $file ) {
492		return true;
493	}
494
495	/**
496	 * Gets the file's last access time.
497	 *
498	 * @since 2.5.0
499	 *
500	 * @param string $file Path to file.
501	 * @return int|false Unix timestamp representing last access time, false on failure.
502	 */
503	public function atime( $file ) {
504		return false;
505	}
506
507	/**
508	 * Gets the file modification time.
509	 *
510	 * @since 2.5.0
511	 *
512	 * @param string $file Path to file.
513	 * @return int|false Unix timestamp representing modification time, false on failure.
514	 */
515	public function mtime( $file ) {
516		return $this->ftp->mdtm( $file );
517	}
518
519	/**
520	 * Gets the file size (in bytes).
521	 *
522	 * @since 2.5.0
523	 *
524	 * @param string $file Path to file.
525	 * @return int|false Size of the file in bytes on success, false on failure.
526	 */
527	public function size( $file ) {
528		return $this->ftp->filesize( $file );
529	}
530
531	/**
532	 * Sets the access and modification times of a file.
533	 *
534	 * Note: If $file doesn't exist, it will be created.
535	 *
536	 * @since 2.5.0
537	 *
538	 * @param string $file  Path to file.
539	 * @param int    $time  Optional. Modified time to set for file.
540	 *                      Default 0.
541	 * @param int    $atime Optional. Access time to set for file.
542	 *                      Default 0.
543	 * @return bool True on success, false on failure.
544	 */
545	public function touch( $file, $time = 0, $atime = 0 ) {
546		return false;
547	}
548
549	/**
550	 * Creates a directory.
551	 *
552	 * @since 2.5.0
553	 *
554	 * @param string           $path  Path for new directory.
555	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
556	 *                                Default false.
557	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
558	 *                                Default false.
559	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
560	 *                                Default false.
561	 * @return bool True on success, false on failure.
562	 */
563	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
564		$path = untrailingslashit( $path );
565
566		if ( empty( $path ) ) {
567			return false;
568		}
569
570		if ( ! $this->ftp->mkdir( $path ) ) {
571			return false;
572		}
573
574		if ( ! $chmod ) {
575			$chmod = FS_CHMOD_DIR;
576		}
577
578		$this->chmod( $path, $chmod );
579
580		return true;
581	}
582
583	/**
584	 * Deletes a directory.
585	 *
586	 * @since 2.5.0
587	 *
588	 * @param string $path      Path to directory.
589	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
590	 *                          Default false.
591	 * @return bool True on success, false on failure.
592	 */
593	public function rmdir( $path, $recursive = false ) {
594		return $this->delete( $path, $recursive );
595	}
596
597	/**
598	 * Gets details for files in a directory or a specific file.
599	 *
600	 * @since 2.5.0
601	 *
602	 * @param string $path           Path to directory or file.
603	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
604	 *                               Default true.
605	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
606	 *                               Default false.
607	 * @return array|false {
608	 *     Array of files. False if unable to list directory contents.
609	 *
610	 *     @type string $name        Name of the file or directory.
611	 *     @type string $perms       *nix representation of permissions.
612	 *     @type int    $permsn      Octal representation of permissions.
613	 *     @type string $owner       Owner name or ID.
614	 *     @type int    $size        Size of file in bytes.
615	 *     @type int    $lastmodunix Last modified unix timestamp.
616	 *     @type mixed  $lastmod     Last modified month (3 letter) and day (without leading 0).
617	 *     @type int    $time        Last modified time.
618	 *     @type string $type        Type of resource. 'f' for file, 'd' for directory.
619	 *     @type mixed  $files       If a directory and $recursive is true, contains another array of files.
620	 * }
621	 */
622	public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
623		if ( $this->is_file( $path ) ) {
624			$limit_file = basename( $path );
625			$path       = dirname( $path ) . '/';
626		} else {
627			$limit_file = false;
628		}
629
630		mbstring_binary_safe_encoding();
631
632		$list = $this->ftp->dirlist( $path );
633
634		if ( empty( $list ) && ! $this->exists( $path ) ) {
635
636			reset_mbstring_encoding();
637
638			return false;
639		}
640
641		$ret = array();
642
643		foreach ( $list as $struc ) {
644
645			if ( '.' === $struc['name'] || '..' === $struc['name'] ) {
646				continue;
647			}
648
649			if ( ! $include_hidden && '.' === $struc['name'][0] ) {
650				continue;
651			}
652
653			if ( $limit_file && $struc['name'] !== $limit_file ) {
654				continue;
655			}
656
657			if ( 'd' === $struc['type'] ) {
658				if ( $recursive ) {
659					$struc['files'] = $this->dirlist( $path . '/' . $struc['name'], $include_hidden, $recursive );
660				} else {
661					$struc['files'] = array();
662				}
663			}
664
665			// Replace symlinks formatted as "source -> target" with just the source name.
666			if ( $struc['islink'] ) {
667				$struc['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] );
668			}
669
670			// Add the octal representation of the file permissions.
671			$struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] );
672
673			$ret[ $struc['name'] ] = $struc;
674		}
675
676		reset_mbstring_encoding();
677
678		return $ret;
679	}
680
681	/**
682	 * Destructor.
683	 *
684	 * @since 2.5.0
685	 */
686	public function __destruct() {
687		$this->ftp->quit();
688	}
689}
690