ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-filesystem-ftpext.php (20138B)


      1 <?php
      2 /**
      3  * WordPress FTP Filesystem.
      4  *
      5  * @package WordPress
      6  * @subpackage Filesystem
      7  */
      8 
      9 /**
     10  * WordPress Filesystem Class for implementing FTP.
     11  *
     12  * @since 2.5.0
     13  *
     14  * @see WP_Filesystem_Base
     15  */
     16 class WP_Filesystem_FTPext extends WP_Filesystem_Base {
     17 
     18 	/**
     19 	 * @since 2.5.0
     20 	 * @var resource
     21 	 */
     22 	public $link;
     23 
     24 	/**
     25 	 * Constructor.
     26 	 *
     27 	 * @since 2.5.0
     28 	 *
     29 	 * @param array $opt
     30 	 */
     31 	public function __construct( $opt = '' ) {
     32 		$this->method = 'ftpext';
     33 		$this->errors = new WP_Error();
     34 
     35 		// Check if possible to use ftp functions.
     36 		if ( ! extension_loaded( 'ftp' ) ) {
     37 			$this->errors->add( 'no_ftp_ext', __( 'The ftp PHP extension is not available' ) );
     38 			return;
     39 		}
     40 
     41 		// This class uses the timeout on a per-connection basis, others use it on a per-action basis.
     42 		if ( ! defined( 'FS_TIMEOUT' ) ) {
     43 			define( 'FS_TIMEOUT', 240 );
     44 		}
     45 
     46 		if ( empty( $opt['port'] ) ) {
     47 			$this->options['port'] = 21;
     48 		} else {
     49 			$this->options['port'] = $opt['port'];
     50 		}
     51 
     52 		if ( empty( $opt['hostname'] ) ) {
     53 			$this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) );
     54 		} else {
     55 			$this->options['hostname'] = $opt['hostname'];
     56 		}
     57 
     58 		// Check if the options provided are OK.
     59 		if ( empty( $opt['username'] ) ) {
     60 			$this->errors->add( 'empty_username', __( 'FTP username is required' ) );
     61 		} else {
     62 			$this->options['username'] = $opt['username'];
     63 		}
     64 
     65 		if ( empty( $opt['password'] ) ) {
     66 			$this->errors->add( 'empty_password', __( 'FTP password is required' ) );
     67 		} else {
     68 			$this->options['password'] = $opt['password'];
     69 		}
     70 
     71 		$this->options['ssl'] = false;
     72 
     73 		if ( isset( $opt['connection_type'] ) && 'ftps' === $opt['connection_type'] ) {
     74 			$this->options['ssl'] = true;
     75 		}
     76 	}
     77 
     78 	/**
     79 	 * Connects filesystem.
     80 	 *
     81 	 * @since 2.5.0
     82 	 *
     83 	 * @return bool True on success, false on failure.
     84 	 */
     85 	public function connect() {
     86 		if ( isset( $this->options['ssl'] ) && $this->options['ssl'] && function_exists( 'ftp_ssl_connect' ) ) {
     87 			$this->link = @ftp_ssl_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT );
     88 		} else {
     89 			$this->link = @ftp_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT );
     90 		}
     91 
     92 		if ( ! $this->link ) {
     93 			$this->errors->add(
     94 				'connect',
     95 				sprintf(
     96 					/* translators: %s: hostname:port */
     97 					__( 'Failed to connect to FTP Server %s' ),
     98 					$this->options['hostname'] . ':' . $this->options['port']
     99 				)
    100 			);
    101 
    102 			return false;
    103 		}
    104 
    105 		if ( ! @ftp_login( $this->link, $this->options['username'], $this->options['password'] ) ) {
    106 			$this->errors->add(
    107 				'auth',
    108 				sprintf(
    109 					/* translators: %s: Username. */
    110 					__( 'Username/Password incorrect for %s' ),
    111 					$this->options['username']
    112 				)
    113 			);
    114 
    115 			return false;
    116 		}
    117 
    118 		// Set the connection to use Passive FTP.
    119 		ftp_pasv( $this->link, true );
    120 
    121 		if ( @ftp_get_option( $this->link, FTP_TIMEOUT_SEC ) < FS_TIMEOUT ) {
    122 			@ftp_set_option( $this->link, FTP_TIMEOUT_SEC, FS_TIMEOUT );
    123 		}
    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 		$tempfile   = wp_tempnam( $file );
    139 		$temphandle = fopen( $tempfile, 'w+' );
    140 
    141 		if ( ! $temphandle ) {
    142 			unlink( $tempfile );
    143 			return false;
    144 		}
    145 
    146 		if ( ! ftp_fget( $this->link, $temphandle, $file, FTP_BINARY ) ) {
    147 			fclose( $temphandle );
    148 			unlink( $tempfile );
    149 			return false;
    150 		}
    151 
    152 		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
    153 		$contents = '';
    154 
    155 		while ( ! feof( $temphandle ) ) {
    156 			$contents .= fread( $temphandle, 8 * KB_IN_BYTES );
    157 		}
    158 
    159 		fclose( $temphandle );
    160 		unlink( $tempfile );
    161 
    162 		return $contents;
    163 	}
    164 
    165 	/**
    166 	 * Reads entire file into an array.
    167 	 *
    168 	 * @since 2.5.0
    169 	 *
    170 	 * @param string $file Path to the file.
    171 	 * @return array|false File contents in an array on success, false on failure.
    172 	 */
    173 	public function get_contents_array( $file ) {
    174 		return explode( "\n", $this->get_contents( $file ) );
    175 	}
    176 
    177 	/**
    178 	 * Writes a string to a file.
    179 	 *
    180 	 * @since 2.5.0
    181 	 *
    182 	 * @param string    $file     Remote path to the file where to write the data.
    183 	 * @param string    $contents The data to write.
    184 	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
    185 	 *                            Default false.
    186 	 * @return bool True on success, false on failure.
    187 	 */
    188 	public function put_contents( $file, $contents, $mode = false ) {
    189 		$tempfile   = wp_tempnam( $file );
    190 		$temphandle = fopen( $tempfile, 'wb+' );
    191 
    192 		if ( ! $temphandle ) {
    193 			unlink( $tempfile );
    194 			return false;
    195 		}
    196 
    197 		mbstring_binary_safe_encoding();
    198 
    199 		$data_length   = strlen( $contents );
    200 		$bytes_written = fwrite( $temphandle, $contents );
    201 
    202 		reset_mbstring_encoding();
    203 
    204 		if ( $data_length !== $bytes_written ) {
    205 			fclose( $temphandle );
    206 			unlink( $tempfile );
    207 			return false;
    208 		}
    209 
    210 		fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
    211 
    212 		$ret = ftp_fput( $this->link, $file, $temphandle, FTP_BINARY );
    213 
    214 		fclose( $temphandle );
    215 		unlink( $tempfile );
    216 
    217 		$this->chmod( $file, $mode );
    218 
    219 		return $ret;
    220 	}
    221 
    222 	/**
    223 	 * Gets the current working directory.
    224 	 *
    225 	 * @since 2.5.0
    226 	 *
    227 	 * @return string|false The current working directory on success, false on failure.
    228 	 */
    229 	public function cwd() {
    230 		$cwd = ftp_pwd( $this->link );
    231 
    232 		if ( $cwd ) {
    233 			$cwd = trailingslashit( $cwd );
    234 		}
    235 
    236 		return $cwd;
    237 	}
    238 
    239 	/**
    240 	 * Changes current directory.
    241 	 *
    242 	 * @since 2.5.0
    243 	 *
    244 	 * @param string $dir The new current directory.
    245 	 * @return bool True on success, false on failure.
    246 	 */
    247 	public function chdir( $dir ) {
    248 		return @ftp_chdir( $this->link, $dir );
    249 	}
    250 
    251 	/**
    252 	 * Changes filesystem permissions.
    253 	 *
    254 	 * @since 2.5.0
    255 	 *
    256 	 * @param string    $file      Path to the file.
    257 	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
    258 	 *                             0755 for directories. Default false.
    259 	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
    260 	 *                             Default false.
    261 	 * @return bool True on success, false on failure.
    262 	 */
    263 	public function chmod( $file, $mode = false, $recursive = false ) {
    264 		if ( ! $mode ) {
    265 			if ( $this->is_file( $file ) ) {
    266 				$mode = FS_CHMOD_FILE;
    267 			} elseif ( $this->is_dir( $file ) ) {
    268 				$mode = FS_CHMOD_DIR;
    269 			} else {
    270 				return false;
    271 			}
    272 		}
    273 
    274 		// chmod any sub-objects if recursive.
    275 		if ( $recursive && $this->is_dir( $file ) ) {
    276 			$filelist = $this->dirlist( $file );
    277 
    278 			foreach ( (array) $filelist as $filename => $filemeta ) {
    279 				$this->chmod( $file . '/' . $filename, $mode, $recursive );
    280 			}
    281 		}
    282 
    283 		// chmod the file or directory.
    284 		if ( ! function_exists( 'ftp_chmod' ) ) {
    285 			return (bool) ftp_site( $this->link, sprintf( 'CHMOD %o %s', $mode, $file ) );
    286 		}
    287 
    288 		return (bool) ftp_chmod( $this->link, $mode, $file );
    289 	}
    290 
    291 	/**
    292 	 * Gets the file owner.
    293 	 *
    294 	 * @since 2.5.0
    295 	 *
    296 	 * @param string $file Path to the file.
    297 	 * @return string|false Username of the owner on success, false on failure.
    298 	 */
    299 	public function owner( $file ) {
    300 		$dir = $this->dirlist( $file );
    301 
    302 		return $dir[ $file ]['owner'];
    303 	}
    304 
    305 	/**
    306 	 * Gets the permissions of the specified file or filepath in their octal format.
    307 	 *
    308 	 * @since 2.5.0
    309 	 *
    310 	 * @param string $file Path to the file.
    311 	 * @return string Mode of the file (the last 3 digits).
    312 	 */
    313 	public function getchmod( $file ) {
    314 		$dir = $this->dirlist( $file );
    315 
    316 		return $dir[ $file ]['permsn'];
    317 	}
    318 
    319 	/**
    320 	 * Gets the file's group.
    321 	 *
    322 	 * @since 2.5.0
    323 	 *
    324 	 * @param string $file Path to the file.
    325 	 * @return string|false The group on success, false on failure.
    326 	 */
    327 	public function group( $file ) {
    328 		$dir = $this->dirlist( $file );
    329 
    330 		return $dir[ $file ]['group'];
    331 	}
    332 
    333 	/**
    334 	 * Copies a file.
    335 	 *
    336 	 * @since 2.5.0
    337 	 *
    338 	 * @param string    $source      Path to the source file.
    339 	 * @param string    $destination Path to the destination file.
    340 	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
    341 	 *                               Default false.
    342 	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
    343 	 *                               0755 for dirs. Default false.
    344 	 * @return bool True on success, false on failure.
    345 	 */
    346 	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
    347 		if ( ! $overwrite && $this->exists( $destination ) ) {
    348 			return false;
    349 		}
    350 
    351 		$content = $this->get_contents( $source );
    352 
    353 		if ( false === $content ) {
    354 			return false;
    355 		}
    356 
    357 		return $this->put_contents( $destination, $content, $mode );
    358 	}
    359 
    360 	/**
    361 	 * Moves a file.
    362 	 *
    363 	 * @since 2.5.0
    364 	 *
    365 	 * @param string $source      Path to the source file.
    366 	 * @param string $destination Path to the destination file.
    367 	 * @param bool   $overwrite   Optional. Whether to overwrite the destination file if it exists.
    368 	 *                            Default false.
    369 	 * @return bool True on success, false on failure.
    370 	 */
    371 	public function move( $source, $destination, $overwrite = false ) {
    372 		return ftp_rename( $this->link, $source, $destination );
    373 	}
    374 
    375 	/**
    376 	 * Deletes a file or directory.
    377 	 *
    378 	 * @since 2.5.0
    379 	 *
    380 	 * @param string       $file      Path to the file or directory.
    381 	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
    382 	 *                                Default false.
    383 	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
    384 	 *                                Default false.
    385 	 * @return bool True on success, false on failure.
    386 	 */
    387 	public function delete( $file, $recursive = false, $type = false ) {
    388 		if ( empty( $file ) ) {
    389 			return false;
    390 		}
    391 
    392 		if ( 'f' === $type || $this->is_file( $file ) ) {
    393 			return ftp_delete( $this->link, $file );
    394 		}
    395 
    396 		if ( ! $recursive ) {
    397 			return ftp_rmdir( $this->link, $file );
    398 		}
    399 
    400 		$filelist = $this->dirlist( trailingslashit( $file ) );
    401 
    402 		if ( ! empty( $filelist ) ) {
    403 			foreach ( $filelist as $delete_file ) {
    404 				$this->delete( trailingslashit( $file ) . $delete_file['name'], $recursive, $delete_file['type'] );
    405 			}
    406 		}
    407 
    408 		return ftp_rmdir( $this->link, $file );
    409 	}
    410 
    411 	/**
    412 	 * Checks if a file or directory exists.
    413 	 *
    414 	 * @since 2.5.0
    415 	 *
    416 	 * @param string $file Path to file or directory.
    417 	 * @return bool Whether $file exists or not.
    418 	 */
    419 	public function exists( $file ) {
    420 		$list = ftp_nlist( $this->link, $file );
    421 
    422 		if ( empty( $list ) && $this->is_dir( $file ) ) {
    423 			return true; // File is an empty directory.
    424 		}
    425 
    426 		return ! empty( $list ); // Empty list = no file, so invert.
    427 	}
    428 
    429 	/**
    430 	 * Checks if resource is a file.
    431 	 *
    432 	 * @since 2.5.0
    433 	 *
    434 	 * @param string $file File path.
    435 	 * @return bool Whether $file is a file.
    436 	 */
    437 	public function is_file( $file ) {
    438 		return $this->exists( $file ) && ! $this->is_dir( $file );
    439 	}
    440 
    441 	/**
    442 	 * Checks if resource is a directory.
    443 	 *
    444 	 * @since 2.5.0
    445 	 *
    446 	 * @param string $path Directory path.
    447 	 * @return bool Whether $path is a directory.
    448 	 */
    449 	public function is_dir( $path ) {
    450 		$cwd    = $this->cwd();
    451 		$result = @ftp_chdir( $this->link, trailingslashit( $path ) );
    452 
    453 		if ( $result && $path === $this->cwd() || $this->cwd() !== $cwd ) {
    454 			@ftp_chdir( $this->link, $cwd );
    455 			return true;
    456 		}
    457 
    458 		return false;
    459 	}
    460 
    461 	/**
    462 	 * Checks if a file is readable.
    463 	 *
    464 	 * @since 2.5.0
    465 	 *
    466 	 * @param string $file Path to file.
    467 	 * @return bool Whether $file is readable.
    468 	 */
    469 	public function is_readable( $file ) {
    470 		return true;
    471 	}
    472 
    473 	/**
    474 	 * Checks if a file or directory is writable.
    475 	 *
    476 	 * @since 2.5.0
    477 	 *
    478 	 * @param string $file Path to file or directory.
    479 	 * @return bool Whether $file is writable.
    480 	 */
    481 	public function is_writable( $file ) {
    482 		return true;
    483 	}
    484 
    485 	/**
    486 	 * Gets the file's last access time.
    487 	 *
    488 	 * @since 2.5.0
    489 	 *
    490 	 * @param string $file Path to file.
    491 	 * @return int|false Unix timestamp representing last access time, false on failure.
    492 	 */
    493 	public function atime( $file ) {
    494 		return false;
    495 	}
    496 
    497 	/**
    498 	 * Gets the file modification time.
    499 	 *
    500 	 * @since 2.5.0
    501 	 *
    502 	 * @param string $file Path to file.
    503 	 * @return int|false Unix timestamp representing modification time, false on failure.
    504 	 */
    505 	public function mtime( $file ) {
    506 		return ftp_mdtm( $this->link, $file );
    507 	}
    508 
    509 	/**
    510 	 * Gets the file size (in bytes).
    511 	 *
    512 	 * @since 2.5.0
    513 	 *
    514 	 * @param string $file Path to file.
    515 	 * @return int|false Size of the file in bytes on success, false on failure.
    516 	 */
    517 	public function size( $file ) {
    518 		return ftp_size( $this->link, $file );
    519 	}
    520 
    521 	/**
    522 	 * Sets the access and modification times of a file.
    523 	 *
    524 	 * Note: If $file doesn't exist, it will be created.
    525 	 *
    526 	 * @since 2.5.0
    527 	 *
    528 	 * @param string $file  Path to file.
    529 	 * @param int    $time  Optional. Modified time to set for file.
    530 	 *                      Default 0.
    531 	 * @param int    $atime Optional. Access time to set for file.
    532 	 *                      Default 0.
    533 	 * @return bool True on success, false on failure.
    534 	 */
    535 	public function touch( $file, $time = 0, $atime = 0 ) {
    536 		return false;
    537 	}
    538 
    539 	/**
    540 	 * Creates a directory.
    541 	 *
    542 	 * @since 2.5.0
    543 	 *
    544 	 * @param string           $path  Path for new directory.
    545 	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
    546 	 *                                Default false.
    547 	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
    548 	 *                                Default false.
    549 	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
    550 	 *                                Default false.
    551 	 * @return bool True on success, false on failure.
    552 	 */
    553 	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
    554 		$path = untrailingslashit( $path );
    555 
    556 		if ( empty( $path ) ) {
    557 			return false;
    558 		}
    559 
    560 		if ( ! ftp_mkdir( $this->link, $path ) ) {
    561 			return false;
    562 		}
    563 
    564 		$this->chmod( $path, $chmod );
    565 
    566 		return true;
    567 	}
    568 
    569 	/**
    570 	 * Deletes a directory.
    571 	 *
    572 	 * @since 2.5.0
    573 	 *
    574 	 * @param string $path      Path to directory.
    575 	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
    576 	 *                          Default false.
    577 	 * @return bool True on success, false on failure.
    578 	 */
    579 	public function rmdir( $path, $recursive = false ) {
    580 		return $this->delete( $path, $recursive );
    581 	}
    582 
    583 	/**
    584 	 * @param string $line
    585 	 * @return array
    586 	 */
    587 	public function parselisting( $line ) {
    588 		static $is_windows = null;
    589 
    590 		if ( is_null( $is_windows ) ) {
    591 			$is_windows = stripos( ftp_systype( $this->link ), 'win' ) !== false;
    592 		}
    593 
    594 		if ( $is_windows && preg_match( '/([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|<DIR>) +(.+)/', $line, $lucifer ) ) {
    595 			$b = array();
    596 
    597 			if ( $lucifer[3] < 70 ) {
    598 				$lucifer[3] += 2000;
    599 			} else {
    600 				$lucifer[3] += 1900; // 4-digit year fix.
    601 			}
    602 
    603 			$b['isdir'] = ( '<DIR>' === $lucifer[7] );
    604 
    605 			if ( $b['isdir'] ) {
    606 				$b['type'] = 'd';
    607 			} else {
    608 				$b['type'] = 'f';
    609 			}
    610 
    611 			$b['size']   = $lucifer[7];
    612 			$b['month']  = $lucifer[1];
    613 			$b['day']    = $lucifer[2];
    614 			$b['year']   = $lucifer[3];
    615 			$b['hour']   = $lucifer[4];
    616 			$b['minute'] = $lucifer[5];
    617 			$b['time']   = mktime( $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) === 0 ? 12 : 0 ), $lucifer[5], 0, $lucifer[1], $lucifer[2], $lucifer[3] );
    618 			$b['am/pm']  = $lucifer[6];
    619 			$b['name']   = $lucifer[8];
    620 		} elseif ( ! $is_windows ) {
    621 			$lucifer = preg_split( '/[ ]/', $line, 9, PREG_SPLIT_NO_EMPTY );
    622 
    623 			if ( $lucifer ) {
    624 				// echo $line."\n";
    625 				$lcount = count( $lucifer );
    626 
    627 				if ( $lcount < 8 ) {
    628 					return '';
    629 				}
    630 
    631 				$b           = array();
    632 				$b['isdir']  = 'd' === $lucifer[0][0];
    633 				$b['islink'] = 'l' === $lucifer[0][0];
    634 
    635 				if ( $b['isdir'] ) {
    636 					$b['type'] = 'd';
    637 				} elseif ( $b['islink'] ) {
    638 					$b['type'] = 'l';
    639 				} else {
    640 					$b['type'] = 'f';
    641 				}
    642 
    643 				$b['perms']  = $lucifer[0];
    644 				$b['permsn'] = $this->getnumchmodfromh( $b['perms'] );
    645 				$b['number'] = $lucifer[1];
    646 				$b['owner']  = $lucifer[2];
    647 				$b['group']  = $lucifer[3];
    648 				$b['size']   = $lucifer[4];
    649 
    650 				if ( 8 === $lcount ) {
    651 					sscanf( $lucifer[5], '%d-%d-%d', $b['year'], $b['month'], $b['day'] );
    652 					sscanf( $lucifer[6], '%d:%d', $b['hour'], $b['minute'] );
    653 
    654 					$b['time'] = mktime( $b['hour'], $b['minute'], 0, $b['month'], $b['day'], $b['year'] );
    655 					$b['name'] = $lucifer[7];
    656 				} else {
    657 					$b['month'] = $lucifer[5];
    658 					$b['day']   = $lucifer[6];
    659 
    660 					if ( preg_match( '/([0-9]{2}):([0-9]{2})/', $lucifer[7], $l2 ) ) {
    661 						$b['year']   = gmdate( 'Y' );
    662 						$b['hour']   = $l2[1];
    663 						$b['minute'] = $l2[2];
    664 					} else {
    665 						$b['year']   = $lucifer[7];
    666 						$b['hour']   = 0;
    667 						$b['minute'] = 0;
    668 					}
    669 
    670 					$b['time'] = strtotime( sprintf( '%d %s %d %02d:%02d', $b['day'], $b['month'], $b['year'], $b['hour'], $b['minute'] ) );
    671 					$b['name'] = $lucifer[8];
    672 				}
    673 			}
    674 		}
    675 
    676 		// Replace symlinks formatted as "source -> target" with just the source name.
    677 		if ( isset( $b['islink'] ) && $b['islink'] ) {
    678 			$b['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] );
    679 		}
    680 
    681 		return $b;
    682 	}
    683 
    684 	/**
    685 	 * Gets details for files in a directory or a specific file.
    686 	 *
    687 	 * @since 2.5.0
    688 	 *
    689 	 * @param string $path           Path to directory or file.
    690 	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
    691 	 *                               Default true.
    692 	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
    693 	 *                               Default false.
    694 	 * @return array|false {
    695 	 *     Array of files. False if unable to list directory contents.
    696 	 *
    697 	 *     @type string $name        Name of the file or directory.
    698 	 *     @type string $perms       *nix representation of permissions.
    699 	 *     @type int    $permsn      Octal representation of permissions.
    700 	 *     @type string $owner       Owner name or ID.
    701 	 *     @type int    $size        Size of file in bytes.
    702 	 *     @type int    $lastmodunix Last modified unix timestamp.
    703 	 *     @type mixed  $lastmod     Last modified month (3 letter) and day (without leading 0).
    704 	 *     @type int    $time        Last modified time.
    705 	 *     @type string $type        Type of resource. 'f' for file, 'd' for directory.
    706 	 *     @type mixed  $files       If a directory and $recursive is true, contains another array of files.
    707 	 * }
    708 	 */
    709 	public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
    710 		if ( $this->is_file( $path ) ) {
    711 			$limit_file = basename( $path );
    712 			$path       = dirname( $path ) . '/';
    713 		} else {
    714 			$limit_file = false;
    715 		}
    716 
    717 		$pwd = ftp_pwd( $this->link );
    718 
    719 		if ( ! @ftp_chdir( $this->link, $path ) ) { // Can't change to folder = folder doesn't exist.
    720 			return false;
    721 		}
    722 
    723 		$list = ftp_rawlist( $this->link, '-a', false );
    724 
    725 		@ftp_chdir( $this->link, $pwd );
    726 
    727 		if ( empty( $list ) ) { // Empty array = non-existent folder (real folder will show . at least).
    728 			return false;
    729 		}
    730 
    731 		$dirlist = array();
    732 
    733 		foreach ( $list as $k => $v ) {
    734 			$entry = $this->parselisting( $v );
    735 
    736 			if ( empty( $entry ) ) {
    737 				continue;
    738 			}
    739 
    740 			if ( '.' === $entry['name'] || '..' === $entry['name'] ) {
    741 				continue;
    742 			}
    743 
    744 			if ( ! $include_hidden && '.' === $entry['name'][0] ) {
    745 				continue;
    746 			}
    747 
    748 			if ( $limit_file && $entry['name'] !== $limit_file ) {
    749 				continue;
    750 			}
    751 
    752 			$dirlist[ $entry['name'] ] = $entry;
    753 		}
    754 
    755 		$ret = array();
    756 
    757 		foreach ( (array) $dirlist as $struc ) {
    758 			if ( 'd' === $struc['type'] ) {
    759 				if ( $recursive ) {
    760 					$struc['files'] = $this->dirlist( $path . '/' . $struc['name'], $include_hidden, $recursive );
    761 				} else {
    762 					$struc['files'] = array();
    763 				}
    764 			}
    765 
    766 			$ret[ $struc['name'] ] = $struc;
    767 		}
    768 
    769 		return $ret;
    770 	}
    771 
    772 	/**
    773 	 * Destructor.
    774 	 *
    775 	 * @since 2.5.0
    776 	 */
    777 	public function __destruct() {
    778 		if ( $this->link ) {
    779 			ftp_close( $this->link );
    780 		}
    781 	}
    782 }