1<?php
2/**
3 * WordPress Filesystem Class for implementing SSH2
4 *
5 * To use this class you must follow these steps for PHP 5.2.6+
6 *
7 * @contrib http://kevin.vanzonneveld.net/techblog/article/make_ssh_connections_with_php/ - Installation Notes
8 *
9 * Compile libssh2 (Note: Only 0.14 is officaly working with PHP 5.2.6+ right now, But many users have found the latest versions work)
10 *
11 * cd /usr/src
12 * wget https://www.libssh2.org/download/libssh2-0.14.tar.gz
13 * tar -zxvf libssh2-0.14.tar.gz
14 * cd libssh2-0.14/
15 * ./configure
16 * make all install
17 *
18 * Note: Do not leave the directory yet!
19 *
20 * Enter: pecl install -f ssh2
21 *
22 * Copy the ssh.so file it creates to your PHP Module Directory.
23 * Open up your PHP.INI file and look for where extensions are placed.
24 * Add in your PHP.ini file: extension=ssh2.so
25 *
26 * Restart Apache!
27 * Check phpinfo() streams to confirm that: ssh2.shell, ssh2.exec, ssh2.tunnel, ssh2.scp, ssh2.sftp  exist.
28 *
29 * Note: As of WordPress 2.8, this utilises the PHP5+ function `stream_get_contents()`.
30 *
31 * @since 2.7.0
32 *
33 * @package WordPress
34 * @subpackage Filesystem
35 */
36class WP_Filesystem_SSH2 extends WP_Filesystem_Base {
37
38	/**
39	 * @since 2.7.0
40	 * @var resource
41	 */
42	public $link = false;
43
44	/**
45	 * @since 2.7.0
46	 * @var resource
47	 */
48	public $sftp_link;
49
50	/**
51	 * @since 2.7.0
52	 * @var bool
53	 */
54	public $keys = false;
55
56	/**
57	 * Constructor.
58	 *
59	 * @since 2.7.0
60	 *
61	 * @param array $opt
62	 */
63	public function __construct( $opt = '' ) {
64		$this->method = 'ssh2';
65		$this->errors = new WP_Error();
66
67		// Check if possible to use ssh2 functions.
68		if ( ! extension_loaded( 'ssh2' ) ) {
69			$this->errors->add( 'no_ssh2_ext', __( 'The ssh2 PHP extension is not available' ) );
70			return;
71		}
72
73		// Set defaults:
74		if ( empty( $opt['port'] ) ) {
75			$this->options['port'] = 22;
76		} else {
77			$this->options['port'] = $opt['port'];
78		}
79
80		if ( empty( $opt['hostname'] ) ) {
81			$this->errors->add( 'empty_hostname', __( 'SSH2 hostname is required' ) );
82		} else {
83			$this->options['hostname'] = $opt['hostname'];
84		}
85
86		// Check if the options provided are OK.
87		if ( ! empty( $opt['public_key'] ) && ! empty( $opt['private_key'] ) ) {
88			$this->options['public_key']  = $opt['public_key'];
89			$this->options['private_key'] = $opt['private_key'];
90
91			$this->options['hostkey'] = array( 'hostkey' => 'ssh-rsa' );
92
93			$this->keys = true;
94		} elseif ( empty( $opt['username'] ) ) {
95			$this->errors->add( 'empty_username', __( 'SSH2 username is required' ) );
96		}
97
98		if ( ! empty( $opt['username'] ) ) {
99			$this->options['username'] = $opt['username'];
100		}
101
102		if ( empty( $opt['password'] ) ) {
103			// Password can be blank if we are using keys.
104			if ( ! $this->keys ) {
105				$this->errors->add( 'empty_password', __( 'SSH2 password is required' ) );
106			}
107		} else {
108			$this->options['password'] = $opt['password'];
109		}
110	}
111
112	/**
113	 * Connects filesystem.
114	 *
115	 * @since 2.7.0
116	 *
117	 * @return bool True on success, false on failure.
118	 */
119	public function connect() {
120		if ( ! $this->keys ) {
121			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'] );
122		} else {
123			$this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'], $this->options['hostkey'] );
124		}
125
126		if ( ! $this->link ) {
127			$this->errors->add(
128				'connect',
129				sprintf(
130					/* translators: %s: hostname:port */
131					__( 'Failed to connect to SSH2 Server %s' ),
132					$this->options['hostname'] . ':' . $this->options['port']
133				)
134			);
135
136			return false;
137		}
138
139		if ( ! $this->keys ) {
140			if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) {
141				$this->errors->add(
142					'auth',
143					sprintf(
144						/* translators: %s: Username. */
145						__( 'Username/Password incorrect for %s' ),
146						$this->options['username']
147					)
148				);
149
150				return false;
151			}
152		} else {
153			if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'], $this->options['private_key'], $this->options['password'] ) ) {
154				$this->errors->add(
155					'auth',
156					sprintf(
157						/* translators: %s: Username. */
158						__( 'Public and Private keys incorrect for %s' ),
159						$this->options['username']
160					)
161				);
162
163				return false;
164			}
165		}
166
167		$this->sftp_link = ssh2_sftp( $this->link );
168
169		if ( ! $this->sftp_link ) {
170			$this->errors->add(
171				'connect',
172				sprintf(
173					/* translators: %s: hostname:port */
174					__( 'Failed to initialize a SFTP subsystem session with the SSH2 Server %s' ),
175					$this->options['hostname'] . ':' . $this->options['port']
176				)
177			);
178
179			return false;
180		}
181
182		return true;
183	}
184
185	/**
186	 * Gets the ssh2.sftp PHP stream wrapper path to open for the given file.
187	 *
188	 * This method also works around a PHP bug where the root directory (/) cannot
189	 * be opened by PHP functions, causing a false failure. In order to work around
190	 * this, the path is converted to /./ which is semantically the same as /
191	 * See https://bugs.php.net/bug.php?id=64169 for more details.
192	 *
193	 * @since 4.4.0
194	 *
195	 * @param string $path The File/Directory path on the remote server to return
196	 * @return string The ssh2.sftp:// wrapped path to use.
197	 */
198	public function sftp_path( $path ) {
199		if ( '/' === $path ) {
200			$path = '/./';
201		}
202
203		return 'ssh2.sftp://' . $this->sftp_link . '/' . ltrim( $path, '/' );
204	}
205
206	/**
207	 * @since 2.7.0
208	 *
209	 * @param string $command
210	 * @param bool   $returnbool
211	 * @return bool|string True on success, false on failure. String if the command was executed, `$returnbool`
212	 *                     is false (default), and data from the resulting stream was retrieved.
213	 */
214	public function run_command( $command, $returnbool = false ) {
215		if ( ! $this->link ) {
216			return false;
217		}
218
219		$stream = ssh2_exec( $this->link, $command );
220
221		if ( ! $stream ) {
222			$this->errors->add(
223				'command',
224				sprintf(
225					/* translators: %s: Command. */
226					__( 'Unable to perform command: %s' ),
227					$command
228				)
229			);
230		} else {
231			stream_set_blocking( $stream, true );
232			stream_set_timeout( $stream, FS_TIMEOUT );
233			$data = stream_get_contents( $stream );
234			fclose( $stream );
235
236			if ( $returnbool ) {
237				return ( false === $data ) ? false : '' !== trim( $data );
238			} else {
239				return $data;
240			}
241		}
242
243		return false;
244	}
245
246	/**
247	 * Reads entire file into a string.
248	 *
249	 * @since 2.7.0
250	 *
251	 * @param string $file Name of the file to read.
252	 * @return string|false Read data on success, false if no temporary file could be opened,
253	 *                      or if the file couldn't be retrieved.
254	 */
255	public function get_contents( $file ) {
256		return file_get_contents( $this->sftp_path( $file ) );
257	}
258
259	/**
260	 * Reads entire file into an array.
261	 *
262	 * @since 2.7.0
263	 *
264	 * @param string $file Path to the file.
265	 * @return array|false File contents in an array on success, false on failure.
266	 */
267	public function get_contents_array( $file ) {
268		return file( $this->sftp_path( $file ) );
269	}
270
271	/**
272	 * Writes a string to a file.
273	 *
274	 * @since 2.7.0
275	 *
276	 * @param string    $file     Remote path to the file where to write the data.
277	 * @param string    $contents The data to write.
278	 * @param int|false $mode     Optional. The file permissions as octal number, usually 0644.
279	 *                            Default false.
280	 * @return bool True on success, false on failure.
281	 */
282	public function put_contents( $file, $contents, $mode = false ) {
283		$ret = file_put_contents( $this->sftp_path( $file ), $contents );
284
285		if ( strlen( $contents ) !== $ret ) {
286			return false;
287		}
288
289		$this->chmod( $file, $mode );
290
291		return true;
292	}
293
294	/**
295	 * Gets the current working directory.
296	 *
297	 * @since 2.7.0
298	 *
299	 * @return string|false The current working directory on success, false on failure.
300	 */
301	public function cwd() {
302		$cwd = ssh2_sftp_realpath( $this->sftp_link, '.' );
303
304		if ( $cwd ) {
305			$cwd = trailingslashit( trim( $cwd ) );
306		}
307
308		return $cwd;
309	}
310
311	/**
312	 * Changes current directory.
313	 *
314	 * @since 2.7.0
315	 *
316	 * @param string $dir The new current directory.
317	 * @return bool True on success, false on failure.
318	 */
319	public function chdir( $dir ) {
320		return $this->run_command( 'cd ' . $dir, true );
321	}
322
323	/**
324	 * Changes the file group.
325	 *
326	 * @since 2.7.0
327	 *
328	 * @param string     $file      Path to the file.
329	 * @param string|int $group     A group name or number.
330	 * @param bool       $recursive Optional. If set to true, changes file group recursively.
331	 *                              Default false.
332	 * @return bool True on success, false on failure.
333	 */
334	public function chgrp( $file, $group, $recursive = false ) {
335		if ( ! $this->exists( $file ) ) {
336			return false;
337		}
338
339		if ( ! $recursive || ! $this->is_dir( $file ) ) {
340			return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
341		}
342
343		return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
344	}
345
346	/**
347	 * Changes filesystem permissions.
348	 *
349	 * @since 2.7.0
350	 *
351	 * @param string    $file      Path to the file.
352	 * @param int|false $mode      Optional. The permissions as octal number, usually 0644 for files,
353	 *                             0755 for directories. Default false.
354	 * @param bool      $recursive Optional. If set to true, changes file permissions recursively.
355	 *                             Default false.
356	 * @return bool True on success, false on failure.
357	 */
358	public function chmod( $file, $mode = false, $recursive = false ) {
359		if ( ! $this->exists( $file ) ) {
360			return false;
361		}
362
363		if ( ! $mode ) {
364			if ( $this->is_file( $file ) ) {
365				$mode = FS_CHMOD_FILE;
366			} elseif ( $this->is_dir( $file ) ) {
367				$mode = FS_CHMOD_DIR;
368			} else {
369				return false;
370			}
371		}
372
373		if ( ! $recursive || ! $this->is_dir( $file ) ) {
374			return $this->run_command( sprintf( 'chmod %o %s', $mode, escapeshellarg( $file ) ), true );
375		}
376
377		return $this->run_command( sprintf( 'chmod -R %o %s', $mode, escapeshellarg( $file ) ), true );
378	}
379
380	/**
381	 * Changes the owner of a file or directory.
382	 *
383	 * @since 2.7.0
384	 *
385	 * @param string     $file      Path to the file or directory.
386	 * @param string|int $owner     A user name or number.
387	 * @param bool       $recursive Optional. If set to true, changes file owner recursively.
388	 *                              Default false.
389	 * @return bool True on success, false on failure.
390	 */
391	public function chown( $file, $owner, $recursive = false ) {
392		if ( ! $this->exists( $file ) ) {
393			return false;
394		}
395
396		if ( ! $recursive || ! $this->is_dir( $file ) ) {
397			return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
398		}
399
400		return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
401	}
402
403	/**
404	 * Gets the file owner.
405	 *
406	 * @since 2.7.0
407	 *
408	 * @param string $file Path to the file.
409	 * @return string|false Username of the owner on success, false on failure.
410	 */
411	public function owner( $file ) {
412		$owneruid = @fileowner( $this->sftp_path( $file ) );
413
414		if ( ! $owneruid ) {
415			return false;
416		}
417
418		if ( ! function_exists( 'posix_getpwuid' ) ) {
419			return $owneruid;
420		}
421
422		$ownerarray = posix_getpwuid( $owneruid );
423
424		if ( ! $ownerarray ) {
425			return false;
426		}
427
428		return $ownerarray['name'];
429	}
430
431	/**
432	 * Gets the permissions of the specified file or filepath in their octal format.
433	 *
434	 * @since 2.7.0
435	 *
436	 * @param string $file Path to the file.
437	 * @return string Mode of the file (the last 3 digits).
438	 */
439	public function getchmod( $file ) {
440		return substr( decoct( @fileperms( $this->sftp_path( $file ) ) ), -3 );
441	}
442
443	/**
444	 * Gets the file's group.
445	 *
446	 * @since 2.7.0
447	 *
448	 * @param string $file Path to the file.
449	 * @return string|false The group on success, false on failure.
450	 */
451	public function group( $file ) {
452		$gid = @filegroup( $this->sftp_path( $file ) );
453
454		if ( ! $gid ) {
455			return false;
456		}
457
458		if ( ! function_exists( 'posix_getgrgid' ) ) {
459			return $gid;
460		}
461
462		$grouparray = posix_getgrgid( $gid );
463
464		if ( ! $grouparray ) {
465			return false;
466		}
467
468		return $grouparray['name'];
469	}
470
471	/**
472	 * Copies a file.
473	 *
474	 * @since 2.7.0
475	 *
476	 * @param string    $source      Path to the source file.
477	 * @param string    $destination Path to the destination file.
478	 * @param bool      $overwrite   Optional. Whether to overwrite the destination file if it exists.
479	 *                               Default false.
480	 * @param int|false $mode        Optional. The permissions as octal number, usually 0644 for files,
481	 *                               0755 for dirs. Default false.
482	 * @return bool True on success, false on failure.
483	 */
484	public function copy( $source, $destination, $overwrite = false, $mode = false ) {
485		if ( ! $overwrite && $this->exists( $destination ) ) {
486			return false;
487		}
488
489		$content = $this->get_contents( $source );
490
491		if ( false === $content ) {
492			return false;
493		}
494
495		return $this->put_contents( $destination, $content, $mode );
496	}
497
498	/**
499	 * Moves a file.
500	 *
501	 * @since 2.7.0
502	 *
503	 * @param string $source      Path to the source file.
504	 * @param string $destination Path to the destination file.
505	 * @param bool   $overwrite   Optional. Whether to overwrite the destination file if it exists.
506	 *                            Default false.
507	 * @return bool True on success, false on failure.
508	 */
509	public function move( $source, $destination, $overwrite = false ) {
510		if ( $this->exists( $destination ) ) {
511			if ( $overwrite ) {
512				// We need to remove the destination file before we can rename the source.
513				$this->delete( $destination, false, 'f' );
514			} else {
515				// If we're not overwriting, the rename will fail, so return early.
516				return false;
517			}
518		}
519
520		return ssh2_sftp_rename( $this->sftp_link, $source, $destination );
521	}
522
523	/**
524	 * Deletes a file or directory.
525	 *
526	 * @since 2.7.0
527	 *
528	 * @param string       $file      Path to the file or directory.
529	 * @param bool         $recursive Optional. If set to true, deletes files and folders recursively.
530	 *                                Default false.
531	 * @param string|false $type      Type of resource. 'f' for file, 'd' for directory.
532	 *                                Default false.
533	 * @return bool True on success, false on failure.
534	 */
535	public function delete( $file, $recursive = false, $type = false ) {
536		if ( 'f' === $type || $this->is_file( $file ) ) {
537			return ssh2_sftp_unlink( $this->sftp_link, $file );
538		}
539
540		if ( ! $recursive ) {
541			return ssh2_sftp_rmdir( $this->sftp_link, $file );
542		}
543
544		$filelist = $this->dirlist( $file );
545
546		if ( is_array( $filelist ) ) {
547			foreach ( $filelist as $filename => $fileinfo ) {
548				$this->delete( $file . '/' . $filename, $recursive, $fileinfo['type'] );
549			}
550		}
551
552		return ssh2_sftp_rmdir( $this->sftp_link, $file );
553	}
554
555	/**
556	 * Checks if a file or directory exists.
557	 *
558	 * @since 2.7.0
559	 *
560	 * @param string $file Path to file or directory.
561	 * @return bool Whether $file exists or not.
562	 */
563	public function exists( $file ) {
564		return file_exists( $this->sftp_path( $file ) );
565	}
566
567	/**
568	 * Checks if resource is a file.
569	 *
570	 * @since 2.7.0
571	 *
572	 * @param string $file File path.
573	 * @return bool Whether $file is a file.
574	 */
575	public function is_file( $file ) {
576		return is_file( $this->sftp_path( $file ) );
577	}
578
579	/**
580	 * Checks if resource is a directory.
581	 *
582	 * @since 2.7.0
583	 *
584	 * @param string $path Directory path.
585	 * @return bool Whether $path is a directory.
586	 */
587	public function is_dir( $path ) {
588		return is_dir( $this->sftp_path( $path ) );
589	}
590
591	/**
592	 * Checks if a file is readable.
593	 *
594	 * @since 2.7.0
595	 *
596	 * @param string $file Path to file.
597	 * @return bool Whether $file is readable.
598	 */
599	public function is_readable( $file ) {
600		return is_readable( $this->sftp_path( $file ) );
601	}
602
603	/**
604	 * Checks if a file or directory is writable.
605	 *
606	 * @since 2.7.0
607	 *
608	 * @param string $file Path to file or directory.
609	 * @return bool Whether $file is writable.
610	 */
611	public function is_writable( $file ) {
612		// PHP will base its writable checks on system_user === file_owner, not ssh_user === file_owner.
613		return true;
614	}
615
616	/**
617	 * Gets the file's last access time.
618	 *
619	 * @since 2.7.0
620	 *
621	 * @param string $file Path to file.
622	 * @return int|false Unix timestamp representing last access time, false on failure.
623	 */
624	public function atime( $file ) {
625		return fileatime( $this->sftp_path( $file ) );
626	}
627
628	/**
629	 * Gets the file modification time.
630	 *
631	 * @since 2.7.0
632	 *
633	 * @param string $file Path to file.
634	 * @return int|false Unix timestamp representing modification time, false on failure.
635	 */
636	public function mtime( $file ) {
637		return filemtime( $this->sftp_path( $file ) );
638	}
639
640	/**
641	 * Gets the file size (in bytes).
642	 *
643	 * @since 2.7.0
644	 *
645	 * @param string $file Path to file.
646	 * @return int|false Size of the file in bytes on success, false on failure.
647	 */
648	public function size( $file ) {
649		return filesize( $this->sftp_path( $file ) );
650	}
651
652	/**
653	 * Sets the access and modification times of a file.
654	 *
655	 * Note: Not implemented.
656	 *
657	 * @since 2.7.0
658	 *
659	 * @param string $file  Path to file.
660	 * @param int    $time  Optional. Modified time to set for file.
661	 *                      Default 0.
662	 * @param int    $atime Optional. Access time to set for file.
663	 *                      Default 0.
664	 */
665	public function touch( $file, $time = 0, $atime = 0 ) {
666		// Not implemented.
667	}
668
669	/**
670	 * Creates a directory.
671	 *
672	 * @since 2.7.0
673	 *
674	 * @param string           $path  Path for new directory.
675	 * @param int|false        $chmod Optional. The permissions as octal number (or false to skip chmod).
676	 *                                Default false.
677	 * @param string|int|false $chown Optional. A user name or number (or false to skip chown).
678	 *                                Default false.
679	 * @param string|int|false $chgrp Optional. A group name or number (or false to skip chgrp).
680	 *                                Default false.
681	 * @return bool True on success, false on failure.
682	 */
683	public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
684		$path = untrailingslashit( $path );
685
686		if ( empty( $path ) ) {
687			return false;
688		}
689
690		if ( ! $chmod ) {
691			$chmod = FS_CHMOD_DIR;
692		}
693
694		if ( ! ssh2_sftp_mkdir( $this->sftp_link, $path, $chmod, true ) ) {
695			return false;
696		}
697
698		// Set directory permissions.
699		ssh2_sftp_chmod( $this->sftp_link, $path, $chmod );
700
701		if ( $chown ) {
702			$this->chown( $path, $chown );
703		}
704
705		if ( $chgrp ) {
706			$this->chgrp( $path, $chgrp );
707		}
708
709		return true;
710	}
711
712	/**
713	 * Deletes a directory.
714	 *
715	 * @since 2.7.0
716	 *
717	 * @param string $path      Path to directory.
718	 * @param bool   $recursive Optional. Whether to recursively remove files/directories.
719	 *                          Default false.
720	 * @return bool True on success, false on failure.
721	 */
722	public function rmdir( $path, $recursive = false ) {
723		return $this->delete( $path, $recursive );
724	}
725
726	/**
727	 * Gets details for files in a directory or a specific file.
728	 *
729	 * @since 2.7.0
730	 *
731	 * @param string $path           Path to directory or file.
732	 * @param bool   $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
733	 *                               Default true.
734	 * @param bool   $recursive      Optional. Whether to recursively include file details in nested directories.
735	 *                               Default false.
736	 * @return array|false {
737	 *     Array of files. False if unable to list directory contents.
738	 *
739	 *     @type string $name        Name of the file or directory.
740	 *     @type string $perms       *nix representation of permissions.
741	 *     @type int    $permsn      Octal representation of permissions.
742	 *     @type string $owner       Owner name or ID.
743	 *     @type int    $size        Size of file in bytes.
744	 *     @type int    $lastmodunix Last modified unix timestamp.
745	 *     @type mixed  $lastmod     Last modified month (3 letter) and day (without leading 0).
746	 *     @type int    $time        Last modified time.
747	 *     @type string $type        Type of resource. 'f' for file, 'd' for directory.
748	 *     @type mixed  $files       If a directory and $recursive is true, contains another array of files.
749	 * }
750	 */
751	public function dirlist( $path, $include_hidden = true, $recursive = false ) {
752		if ( $this->is_file( $path ) ) {
753			$limit_file = basename( $path );
754			$path       = dirname( $path );
755		} else {
756			$limit_file = false;
757		}
758
759		if ( ! $this->is_dir( $path ) || ! $this->is_readable( $path ) ) {
760			return false;
761		}
762
763		$ret = array();
764		$dir = dir( $this->sftp_path( $path ) );
765
766		if ( ! $dir ) {
767			return false;
768		}
769
770		while ( false !== ( $entry = $dir->read() ) ) {
771			$struc         = array();
772			$struc['name'] = $entry;
773
774			if ( '.' === $struc['name'] || '..' === $struc['name'] ) {
775				continue; // Do not care about these folders.
776			}
777
778			if ( ! $include_hidden && '.' === $struc['name'][0] ) {
779				continue;
780			}
781
782			if ( $limit_file && $struc['name'] !== $limit_file ) {
783				continue;
784			}
785
786			$struc['perms']       = $this->gethchmod( $path . '/' . $entry );
787			$struc['permsn']      = $this->getnumchmodfromh( $struc['perms'] );
788			$struc['number']      = false;
789			$struc['owner']       = $this->owner( $path . '/' . $entry );
790			$struc['group']       = $this->group( $path . '/' . $entry );
791			$struc['size']        = $this->size( $path . '/' . $entry );
792			$struc['lastmodunix'] = $this->mtime( $path . '/' . $entry );
793			$struc['lastmod']     = gmdate( 'M j', $struc['lastmodunix'] );
794			$struc['time']        = gmdate( 'h:i:s', $struc['lastmodunix'] );
795			$struc['type']        = $this->is_dir( $path . '/' . $entry ) ? 'd' : 'f';
796
797			if ( 'd' === $struc['type'] ) {
798				if ( $recursive ) {
799					$struc['files'] = $this->dirlist( $path . '/' . $struc['name'], $include_hidden, $recursive );
800				} else {
801					$struc['files'] = array();
802				}
803			}
804
805			$ret[ $struc['name'] ] = $struc;
806		}
807
808		$dir->close();
809		unset( $dir );
810
811		return $ret;
812	}
813}
814