1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 0.2.9
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Filesystem;
16:
17: use finfo;
18: use SplFileInfo;
19:
20: /**
21: * Convenience class for reading, writing and appending to files.
22: */
23: class File
24: {
25:
26: /**
27: * Folder object of the file
28: *
29: * @var \Cake\Filesystem\Folder
30: * @link https://book.cakephp.org/3.0/en/core-libraries/file-folder.html
31: */
32: public $Folder;
33:
34: /**
35: * File name
36: *
37: * @var string
38: * https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#Cake\Filesystem\File::$name
39: */
40: public $name;
41:
42: /**
43: * File info
44: *
45: * @var array
46: * https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#Cake\Filesystem\File::$info
47: */
48: public $info = [];
49:
50: /**
51: * Holds the file handler resource if the file is opened
52: *
53: * @var resource|null
54: * https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#Cake\Filesystem\File::$handle
55: */
56: public $handle;
57:
58: /**
59: * Enable locking for file reading and writing
60: *
61: * @var bool|null
62: * https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#Cake\Filesystem\File::$lock
63: */
64: public $lock;
65:
66: /**
67: * Path property
68: *
69: * Current file's absolute path
70: *
71: * @var string|null
72: * https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#Cake\Filesystem\File::$path
73: */
74: public $path;
75:
76: /**
77: * Constructor
78: *
79: * @param string $path Path to file
80: * @param bool $create Create file if it does not exist (if true)
81: * @param int $mode Mode to apply to the folder holding the file
82: * @link https://book.cakephp.org/3.0/en/core-libraries/file-folder.html#file-api
83: */
84: public function __construct($path, $create = false, $mode = 0755)
85: {
86: $splInfo = new SplFileInfo($path);
87: $this->Folder = new Folder($splInfo->getPath(), $create, $mode);
88: if (!is_dir($path)) {
89: $this->name = ltrim($splInfo->getFilename(), '/\\');
90: }
91: $this->pwd();
92: $create && !$this->exists() && $this->safe($path) && $this->create();
93: }
94:
95: /**
96: * Closes the current file if it is opened
97: */
98: public function __destruct()
99: {
100: $this->close();
101: }
102:
103: /**
104: * Creates the file.
105: *
106: * @return bool Success
107: */
108: public function create()
109: {
110: $dir = $this->Folder->pwd();
111:
112: if (is_dir($dir) && is_writable($dir) && !$this->exists() && touch($this->path)) {
113: return true;
114: }
115:
116: return false;
117: }
118:
119: /**
120: * Opens the current file with a given $mode
121: *
122: * @param string $mode A valid 'fopen' mode string (r|w|a ...)
123: * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't
124: * @return bool True on success, false on failure
125: */
126: public function open($mode = 'r', $force = false)
127: {
128: if (!$force && is_resource($this->handle)) {
129: return true;
130: }
131: if ($this->exists() === false && $this->create() === false) {
132: return false;
133: }
134:
135: $this->handle = fopen($this->path, $mode);
136:
137: return is_resource($this->handle);
138: }
139:
140: /**
141: * Return the contents of this file as a string.
142: *
143: * @param string|bool $bytes where to start
144: * @param string $mode A `fread` compatible mode.
145: * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't
146: * @return string|false string on success, false on failure
147: */
148: public function read($bytes = false, $mode = 'rb', $force = false)
149: {
150: if ($bytes === false && $this->lock === null) {
151: return file_get_contents($this->path);
152: }
153: if ($this->open($mode, $force) === false) {
154: return false;
155: }
156: if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) {
157: return false;
158: }
159: if (is_int($bytes)) {
160: return fread($this->handle, $bytes);
161: }
162:
163: $data = '';
164: while (!feof($this->handle)) {
165: $data .= fgets($this->handle, 4096);
166: }
167:
168: if ($this->lock !== null) {
169: flock($this->handle, LOCK_UN);
170: }
171: if ($bytes === false) {
172: $this->close();
173: }
174:
175: return trim($data);
176: }
177:
178: /**
179: * Sets or gets the offset for the currently opened file.
180: *
181: * @param int|bool $offset The $offset in bytes to seek. If set to false then the current offset is returned.
182: * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to
183: * @return int|bool True on success, false on failure (set mode), false on failure or integer offset on success (get mode)
184: */
185: public function offset($offset = false, $seek = SEEK_SET)
186: {
187: if ($offset === false) {
188: if (is_resource($this->handle)) {
189: return ftell($this->handle);
190: }
191: } elseif ($this->open() === true) {
192: return fseek($this->handle, $offset, $seek) === 0;
193: }
194:
195: return false;
196: }
197:
198: /**
199: * Prepares an ASCII string for writing. Converts line endings to the
200: * correct terminator for the current platform. If Windows, "\r\n" will be used,
201: * all other platforms will use "\n"
202: *
203: * @param string $data Data to prepare for writing.
204: * @param bool $forceWindows If true forces Windows new line string.
205: * @return string The with converted line endings.
206: */
207: public static function prepare($data, $forceWindows = false)
208: {
209: $lineBreak = "\n";
210: if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) {
211: $lineBreak = "\r\n";
212: }
213:
214: return strtr($data, ["\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak]);
215: }
216:
217: /**
218: * Write given data to this file.
219: *
220: * @param string $data Data to write to this File.
221: * @param string $mode Mode of writing. {@link https://secure.php.net/fwrite See fwrite()}.
222: * @param bool $force Force the file to open
223: * @return bool Success
224: */
225: public function write($data, $mode = 'w', $force = false)
226: {
227: $success = false;
228: if ($this->open($mode, $force) === true) {
229: if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) {
230: return false;
231: }
232:
233: if (fwrite($this->handle, $data) !== false) {
234: $success = true;
235: }
236: if ($this->lock !== null) {
237: flock($this->handle, LOCK_UN);
238: }
239: }
240:
241: return $success;
242: }
243:
244: /**
245: * Append given data string to this file.
246: *
247: * @param string $data Data to write
248: * @param bool $force Force the file to open
249: * @return bool Success
250: */
251: public function append($data, $force = false)
252: {
253: return $this->write($data, 'a', $force);
254: }
255:
256: /**
257: * Closes the current file if it is opened.
258: *
259: * @return bool True if closing was successful or file was already closed, otherwise false
260: */
261: public function close()
262: {
263: if (!is_resource($this->handle)) {
264: return true;
265: }
266:
267: return fclose($this->handle);
268: }
269:
270: /**
271: * Deletes the file.
272: *
273: * @return bool Success
274: */
275: public function delete()
276: {
277: if (is_resource($this->handle)) {
278: fclose($this->handle);
279: $this->handle = null;
280: }
281: if ($this->exists()) {
282: return unlink($this->path);
283: }
284:
285: return false;
286: }
287:
288: /**
289: * Returns the file info as an array with the following keys:
290: *
291: * - dirname
292: * - basename
293: * - extension
294: * - filename
295: * - filesize
296: * - mime
297: *
298: * @return array File information.
299: */
300: public function info()
301: {
302: if (!$this->info) {
303: $this->info = pathinfo($this->path);
304: }
305: if (!isset($this->info['filename'])) {
306: $this->info['filename'] = $this->name();
307: }
308: if (!isset($this->info['filesize'])) {
309: $this->info['filesize'] = $this->size();
310: }
311: if (!isset($this->info['mime'])) {
312: $this->info['mime'] = $this->mime();
313: }
314:
315: return $this->info;
316: }
317:
318: /**
319: * Returns the file extension.
320: *
321: * @return string|false The file extension, false if extension cannot be extracted.
322: */
323: public function ext()
324: {
325: if (!$this->info) {
326: $this->info();
327: }
328: if (isset($this->info['extension'])) {
329: return $this->info['extension'];
330: }
331:
332: return false;
333: }
334:
335: /**
336: * Returns the file name without extension.
337: *
338: * @return string|false The file name without extension, false if name cannot be extracted.
339: */
340: public function name()
341: {
342: if (!$this->info) {
343: $this->info();
344: }
345: if (isset($this->info['extension'])) {
346: return static::_basename($this->name, '.' . $this->info['extension']);
347: }
348: if ($this->name) {
349: return $this->name;
350: }
351:
352: return false;
353: }
354:
355: /**
356: * Returns the file basename. simulate the php basename() for multibyte (mb_basename).
357: *
358: * @param string $path Path to file
359: * @param string|null $ext The name of the extension
360: * @return string the file basename.
361: */
362: protected static function _basename($path, $ext = null)
363: {
364: // check for multibyte string and use basename() if not found
365: if (mb_strlen($path) === strlen($path)) {
366: return ($ext === null)? basename($path) : basename($path, $ext);
367: }
368:
369: $splInfo = new SplFileInfo($path);
370: $name = ltrim($splInfo->getFilename(), '/\\');
371:
372: if ($ext === null || $ext === '') {
373: return $name;
374: }
375: $ext = preg_quote($ext);
376: $new = preg_replace("/({$ext})$/u", "", $name);
377:
378: // basename of '/etc/.d' is '.d' not ''
379: return ($new === '')? $name : $new;
380: }
381:
382: /**
383: * Makes file name safe for saving
384: *
385: * @param string|null $name The name of the file to make safe if different from $this->name
386: * @param string|null $ext The name of the extension to make safe if different from $this->ext
387: * @return string The extension of the file
388: */
389: public function safe($name = null, $ext = null)
390: {
391: if (!$name) {
392: $name = $this->name;
393: }
394: if (!$ext) {
395: $ext = $this->ext();
396: }
397:
398: return preg_replace("/(?:[^\w\.-]+)/", '_', static::_basename($name, $ext));
399: }
400:
401: /**
402: * Get md5 Checksum of file with previous check of Filesize
403: *
404: * @param int|bool $maxsize in MB or true to force
405: * @return string|false md5 Checksum {@link https://secure.php.net/md5_file See md5_file()}, or false in case of an error
406: */
407: public function md5($maxsize = 5)
408: {
409: if ($maxsize === true) {
410: return md5_file($this->path);
411: }
412:
413: $size = $this->size();
414: if ($size && $size < ($maxsize * 1024) * 1024) {
415: return md5_file($this->path);
416: }
417:
418: return false;
419: }
420:
421: /**
422: * Returns the full path of the file.
423: *
424: * @return string Full path to the file
425: */
426: public function pwd()
427: {
428: if ($this->path === null) {
429: $dir = $this->Folder->pwd();
430: if (is_dir($dir)) {
431: $this->path = $this->Folder->slashTerm($dir) . $this->name;
432: }
433: }
434:
435: return $this->path;
436: }
437:
438: /**
439: * Returns true if the file exists.
440: *
441: * @return bool True if it exists, false otherwise
442: */
443: public function exists()
444: {
445: $this->clearStatCache();
446:
447: return (file_exists($this->path) && is_file($this->path));
448: }
449:
450: /**
451: * Returns the "chmod" (permissions) of the file.
452: *
453: * @return string|false Permissions for the file, or false in case of an error
454: */
455: public function perms()
456: {
457: if ($this->exists()) {
458: return substr(sprintf('%o', fileperms($this->path)), -4);
459: }
460:
461: return false;
462: }
463:
464: /**
465: * Returns the file size
466: *
467: * @return int|false Size of the file in bytes, or false in case of an error
468: */
469: public function size()
470: {
471: if ($this->exists()) {
472: return filesize($this->path);
473: }
474:
475: return false;
476: }
477:
478: /**
479: * Returns true if the file is writable.
480: *
481: * @return bool True if it's writable, false otherwise
482: */
483: public function writable()
484: {
485: return is_writable($this->path);
486: }
487:
488: /**
489: * Returns true if the File is executable.
490: *
491: * @return bool True if it's executable, false otherwise
492: */
493: public function executable()
494: {
495: return is_executable($this->path);
496: }
497:
498: /**
499: * Returns true if the file is readable.
500: *
501: * @return bool True if file is readable, false otherwise
502: */
503: public function readable()
504: {
505: return is_readable($this->path);
506: }
507:
508: /**
509: * Returns the file's owner.
510: *
511: * @return int|false The file owner, or false in case of an error
512: */
513: public function owner()
514: {
515: if ($this->exists()) {
516: return fileowner($this->path);
517: }
518:
519: return false;
520: }
521:
522: /**
523: * Returns the file's group.
524: *
525: * @return int|false The file group, or false in case of an error
526: */
527: public function group()
528: {
529: if ($this->exists()) {
530: return filegroup($this->path);
531: }
532:
533: return false;
534: }
535:
536: /**
537: * Returns last access time.
538: *
539: * @return int|false Timestamp of last access time, or false in case of an error
540: */
541: public function lastAccess()
542: {
543: if ($this->exists()) {
544: return fileatime($this->path);
545: }
546:
547: return false;
548: }
549:
550: /**
551: * Returns last modified time.
552: *
553: * @return int|false Timestamp of last modification, or false in case of an error
554: */
555: public function lastChange()
556: {
557: if ($this->exists()) {
558: return filemtime($this->path);
559: }
560:
561: return false;
562: }
563:
564: /**
565: * Returns the current folder.
566: *
567: * @return \Cake\Filesystem\Folder Current folder
568: */
569: public function folder()
570: {
571: return $this->Folder;
572: }
573:
574: /**
575: * Copy the File to $dest
576: *
577: * @param string $dest Absolute path to copy the file to.
578: * @param bool $overwrite Overwrite $dest if exists
579: * @return bool Success
580: */
581: public function copy($dest, $overwrite = true)
582: {
583: if (!$this->exists() || is_file($dest) && !$overwrite) {
584: return false;
585: }
586:
587: return copy($this->path, $dest);
588: }
589:
590: /**
591: * Gets the mime type of the file. Uses the finfo extension if
592: * it's available, otherwise falls back to mime_content_type().
593: *
594: * @return false|string The mimetype of the file, or false if reading fails.
595: */
596: public function mime()
597: {
598: if (!$this->exists()) {
599: return false;
600: }
601: if (class_exists('finfo')) {
602: $finfo = new finfo(FILEINFO_MIME);
603: $type = $finfo->file($this->pwd());
604: if (!$type) {
605: return false;
606: }
607: list($type) = explode(';', $type);
608:
609: return $type;
610: }
611: if (function_exists('mime_content_type')) {
612: return mime_content_type($this->pwd());
613: }
614:
615: return false;
616: }
617:
618: /**
619: * Clear PHP's internal stat cache
620: *
621: * @param bool $all Clear all cache or not. Passing false will clear
622: * the stat cache for the current path only.
623: * @return void
624: */
625: public function clearStatCache($all = false)
626: {
627: if ($all === false) {
628: clearstatcache(true, $this->path);
629: }
630:
631: clearstatcache();
632: }
633:
634: /**
635: * Searches for a given text and replaces the text if found.
636: *
637: * @param string|array $search Text(s) to search for.
638: * @param string|array $replace Text(s) to replace with.
639: * @return bool Success
640: */
641: public function replaceText($search, $replace)
642: {
643: if (!$this->open('r+')) {
644: return false;
645: }
646:
647: if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) {
648: return false;
649: }
650:
651: $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true);
652:
653: if ($this->lock !== null) {
654: flock($this->handle, LOCK_UN);
655: }
656: $this->close();
657:
658: return $replaced;
659: }
660: }
661: