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