root/fFile.php

Revision 758, 35.5 kB (checked in by wbond, 1 week ago)

Fixed ticket #397 - fFile, fImage and fDirectory all handle re-used filenames better and produce more usable exceptions when trying to perform actions on objects that have been deleted

InternalBackwardsCompatibilityBreak - fFilesystem::hookExceptionMap() was changed to fFilesystem::hookDeletedMap() and fFilesystem::updateExceptionMap() was changed to fFilesystem::updateDeletedMap()

LineHide Line Numbers
1 <?php
2 /**
3  * Represents a file on the filesystem, also provides static file-related methods
4  *
5  * @copyright  Copyright (c) 2007-2010 Will Bond, others
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @author     Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
8  * @license    http://flourishlib.com/license
9  *
10  * @package    Flourish
11  * @link       http://flourishlib.com/fFile
12  *
13  * @version    1.0.0b30
14  * @changes    1.0.0b30  Changed the way files deleted in a filesystem transaction are handled, including improvements to the exception that is thrown [wb+wb-imarc, 2010-03-05]
15  * @changes    1.0.0b29  Fixed a couple of undefined variable errors in ::determineMimeTypeByContents() [wb, 2010-03-03]
16  * @changes    1.0.0b28  Added support for some JPEG files created by Photoshop [wb, 2009-12-16]
17  * @changes    1.0.0b27  Backwards Compatibility Break - renamed ::getFilename() to ::getName(), ::getFilesize() to ::getSize(), ::getDirectory() to ::getParent(), added ::move() [wb, 2009-12-16]
18  * @changes    1.0.0b26  ::getDirectory(), ::getFilename() and ::getPath() now all work even if the file has been deleted [wb, 2009-10-22]
19  * @changes    1.0.0b25  Fixed ::__construct() to throw an fValidationException when the file does not exist [wb, 2009-08-21]
20  * @changes    1.0.0b24  Fixed a bug where deleting a file would prevent any future operations in the same script execution on a file or directory with the same path [wb, 2009-08-20]
21  * @changes    1.0.0b23  Added the ability to skip checks in ::__construct() for better performance in conjunction with fFilesystem::createObject() [wb, 2009-08-06]
22  * @changes    1.0.0b22  Fixed ::__toString() to never throw an exception [wb, 2009-08-06]
23  * @changes    1.0.0b21  Fixed a bug in ::determineMimeType() [wb, 2009-07-21]
24  * @changes    1.0.0b20  Fixed the exception message thrown by ::output() when output buffering is turned on [wb, 2009-06-26]
25  * @changes    1.0.0b19  ::rename() will now rename the file in its current directory if the new filename has no directory separator [wb, 2009-05-04]
26  * @changes    1.0.0b18  Changed ::__sleep() to not reset the iterator since it can cause side-effects [wb, 2009-05-04]
27  * @changes    1.0.0b17  Added ::__sleep() and ::__wakeup() for proper serialization with the filesystem map [wb, 2009-05-03]
28  * @changes    1.0.0b16  ::output() now accepts `TRUE` in the second parameter to use the current filename as the attachment filename [wb, 2009-03-23]
29  * @changes    1.0.0b15  Added support for mime type detection of MP3s based on the MPEG-2 (as opposed to MPEG-1) standard [wb, 2009-03-23]
30  * @changes    1.0.0b14  Fixed a bug with detecting the mime type of some MP3s [wb, 2009-03-22]
31  * @changes    1.0.0b13  Fixed a bug with overwriting files via ::rename() on Windows [wb, 2009-03-11]
32  * @changes    1.0.0b12  Backwards compatibility break - Changed the second parameter of ::output() from `$ignore_output_buffer` to `$filename` [wb, 2009-03-05]
33  * @changes    1.0.0b11  Changed ::__clone() and ::duplicate() to copy file permissions to the new file [wb, 2009-01-05]
34  * @changes    1.0.0b10  Fixed ::duplicate() so an exception is not thrown when no parameters are passed [wb, 2009-01-05]
35  * @changes    1.0.0b9   Removed the dependency on fBuffer [wb, 2009-01-05]
36  * @changes    1.0.0b8   Added the Iterator interface, ::output() and ::getMTime() [wb, 2008-12-17]
37  * @changes    1.0.0b7   Removed some unnecessary error suppresion operators [wb, 2008-12-11]
38  * @changes    1.0.0b6   Added the ::__clone() method that duplicates the file on the filesystem when cloned [wb, 2008-12-11]
39  * @changes    1.0.0b5   Fixed detection of mime type for JPEG files with Exif information [wb, 2008-12-04]
40  * @changes    1.0.0b4   Changed the constructor to ensure the path is to a file and not directory [wb, 2008-11-24]
41  * @changes    1.0.0b3   Fixed mime type detection of Microsoft Office files [wb, 2008-11-23]
42  * @changes    1.0.0b2   Made ::rename() and ::write() return the object for method chaining [wb, 2008-11-22]
43  * @changes    1.0.0b    The initial implementation [wb, 2007-06-14]
44  */
45 class fFile implements Iterator
46 {
47     // The following constants allow for nice looking callbacks to static methods
48     const create = 'fFile::create';
49    
50    
51     /**
52     * Creates a file on the filesystem and returns an object representing it.
53     *
54     * This operation will be reverted by a filesystem transaction being rolled back.
55     *
56     * @throws fValidationException  When no file was specified or the file already exists
57     *
58     * @param  string $file_path  The path to the new file
59     * @param  string $contents   The contents to write to the file, must be a non-NULL value to be written
60     * @return fFile
61     */
62     static public function create($file_path, $contents)
63     {
64         if (empty($file_path)) {
65             throw new fValidationException(
66                 'No filename was specified'
67             );
68         }
69        
70         if (file_exists($file_path)) {
71             throw new fValidationException(
72                 'The file specified, %s, already exists',
73                 $file_path
74             );
75         }
76        
77         $directory = fFilesystem::getPathInfo($file_path, 'dirname');
78         if (!is_writable($directory)) {
79             throw new fEnvironmentException(
80                 'The file path specified, %s, is inside of a directory that is not writable',
81                 $file_path
82             );
83         }
84        
85         file_put_contents($file_path, $contents);
86        
87         $file = new fFile($file_path);
88        
89         fFilesystem::recordCreate($file);
90        
91         return $file;
92     }
93    
94    
95     /**
96     * Determines the file's mime type by either looking at the file contents or matching the extension
97     *
98     * Please see the ::getMimeType() description for details about how the
99     * mime type is determined and what mime types are detected.
100     *
101     * @internal
102      *
103     * @param  string $file      The file to check the mime type for - must be a valid filesystem path if no `$contents` are provided, otherwise just a filename
104     * @param  string $contents  The first 4096 bytes of the file content - the `$file` parameter only need be a filename if this is provided
105     * @return string  The mime type of the file
106     */
107     static public function determineMimeType($file, $contents=NULL)
108     {
109         // If no contents are provided, we must get them
110         if ($contents === NULL) {
111             if (!file_exists($file)) {
112                 throw new fValidationException(
113                     'The file specified, %s, does not exist',
114                     $file
115                 );
116             }
117            
118             // The first 4k should be enough for content checking
119             $handle   = fopen($file, 'r');
120             $contents = fread($handle, 4096);
121             fclose($handle);
122         }
123        
124         $extension = strtolower(fFilesystem::getPathInfo($file, 'extension'));
125        
126         // If there are no low ASCII chars and no easily distinguishable tokens, we need to detect by file extension
127         if (!preg_match('#[\x00-\x08\x0B\x0C\x0E-\x1F]|%PDF-|<\?php|\%\!PS-Adobe-3|<\?xml|\{\\\\rtf|<\?=|<html|<\!doctype|<rss|\#\![/a-z0-9]+(python|ruby|perl|php)\b#i', $contents)) {
128             return self::determineMimeTypeByExtension($extension);       
129         }
130        
131         return self::determineMimeTypeByContents($contents, $extension);
132     }
133    
134    
135     /**
136     * Looks for specific bytes in a file to determine the mime type of the file
137     *
138     * @param  string $content    The first 4 bytes of the file content to use for byte checking
139     * @param  string $extension  The extension of the filetype, only used for difficult files such as Microsoft office documents
140     * @return string  The mime type of the file
141     */
142     static private function determineMimeTypeByContents($content, $extension)
143     {
144         $length = strlen($content);
145         $_0_8   = substr($content, 0, 8);
146         $_0_6   = substr($content, 0, 6);
147         $_0_5   = substr($content, 0, 5);
148         $_0_4   = substr($content, 0, 4);
149         $_0_3   = substr($content, 0, 3);
150         $_0_2   = substr($content, 0, 2);
151         $_8_4   = substr($content, 8, 4);
152        
153         // Images
154         if ($_0_4 == "MM\x00\x2A" || $_0_4 == "II\x2A\x00") {
155             return 'image/tiff';   
156         }
157        
158         if ($_0_8 == "\x89PNG\x0D\x0A\x1A\x0A") {
159             return 'image/png';   
160         }
161        
162         if ($_0_4 == 'GIF8') {
163             return 'image/gif';   
164         }
165        
166         if ($_0_2 == 'BM' && strlen($content) > 14 && array($content[14], array("\x0C", "\x28", "\x40", "\x80"))) {
167             return 'image/x-ms-bmp';   
168         }
169        
170         $normal_jpeg    = $length > 10 && in_array(substr($content, 6, 4), array('JFIF', 'Exif'));
171         $photoshop_jpeg = $length > 24 && $_0_4 == "\xFF\xD8\xFF\xED" && substr($content, 20, 4) == '8BIM';
172         if ($normal_jpeg || $photoshop_jpeg) {
173             return 'image/jpeg';   
174         }
175        
176         if (preg_match('#^[^\n\r]*\%\!PS-Adobe-3#', $content)) {
177             return 'application/postscript';           
178         }
179        
180         if ($_0_4 == "\x00\x00\x01\x00") {
181             return 'application/vnd.microsoft.icon';   
182         }
183        
184        
185         // Audio/Video
186         if ($_0_4 == 'MOVI') {
187             if (in_array($_4_4, array('moov', 'mdat'))) {
188                 return 'video/quicktime';
189             }   
190         }
191        
192         if ($length > 8 && substr($content, 4, 4) == 'ftyp') {
193            
194             $_8_3 = substr($content, 8, 3);
195             $_8_2 = substr($content, 8, 2);
196            
197             if (in_array($_8_4, array('isom', 'iso2', 'mp41', 'mp42'))) {
198                 return 'video/mp4';
199             }   
200            
201             if ($_8_3 == 'M4A') {
202                 return 'audio/mp4';
203             }
204            
205             if ($_8_3 == 'M4V') {
206                 return 'video/mp4';
207             }
208            
209             if ($_8_3 == 'M4P' || $_8_3 == 'M4B' || $_8_2 == 'qt') {
210                 return 'video/quicktime';   
211             }
212         }
213        
214         // MP3
215         if (($_0_2 & "\xFF\xF6") == "\xFF\xF2") {
216             if (($content[2] & "\xF0") != "\xF0" && ($content[2] & "\x0C") != "\x0C") {
217                 return 'audio/mpeg';
218             }   
219         }
220         if ($_0_3 == 'ID3') {
221             return 'audio/mpeg';   
222         }
223        
224         if ($_0_8 == "\x30\x26\xB2\x75\x8E\x66\xCF\x11") {
225             if ($content[24] == "\x07") {
226                 return 'audio/x-ms-wma';
227             }
228             if ($content[24] == "\x08") {
229                 return 'video/x-ms-wmv';
230             }
231             return 'video/x-ms-asf';   
232         }
233        
234         if ($_0_4 == 'RIFF' && $_8_4 == 'AVI ') {
235             return 'video/x-msvideo';   
236         }
237        
238         if ($_0_4 == 'RIFF' && $_8_4 == 'WAVE') {
239             return 'audio/x-wav';   
240         }
241        
242         if ($_0_4 == 'OggS') {
243             $_28_5 = substr($content, 28, 5);
244             if ($_28_5 == "\x01\x76\x6F\x72\x62") {
245                 return 'audio/vorbis';   
246             }
247             if ($_28_5 == "\x07\x46\x4C\x41\x43") {
248                 return 'audio/x-flac';   
249             }
250             // Theora and OGM   
251             if ($_28_5 == "\x80\x74\x68\x65\x6F" || $_28_5 == "\x76\x69\x64\x65") {
252                 return 'video/ogg';       
253             }
254         }
255        
256         if ($_0_3 == 'FWS' || $_0_3 == 'CWS') {
257             return 'application/x-shockwave-flash';   
258         }
259        
260         if ($_0_3 == 'FLV') {
261             return 'video/x-flv';   
262         }
263        
264        
265         // Documents
266         if ($_0_5 == '%PDF-') {
267             return 'application/pdf';     
268         }
269        
270         if ($_0_5 == '{\rtf') {
271             return 'text/rtf';   
272         }
273        
274         // Office '97-2003 or Office 2007 formats
275         if ($_0_8 == "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" || $_0_8 == "PK\x03\x04\x14\x00\x06\x00") {
276             if (in_array($extension, array('xlsx', 'xls', 'csv', 'tab'))) {
277                 return 'application/vnd.ms-excel';   
278             }
279             if (in_array($extension, array('pptx', 'ppt'))) {   
280                 return 'application/vnd.ms-powerpoint';
281             }
282             // We default to word since we need something if the extension isn't recognized
283             return 'application/msword';
284         }
285        
286         if ($_0_8 == "\x09\x04\x06\x00\x00\x00\x10\x00") {
287             return 'application/vnd.ms-excel';   
288         }
289        
290         if ($_0_6 == "\xDB\xA5\x2D\x00\x00\x00" || $_0_5 == "\x50\x4F\x5E\x51\x60" || $_0_4 == "\xFE\x37\x0\x23" || $_0_3 == "\x94\xA6\x2E") {
291             return 'application/msword';   
292         }
293        
294        
295         // Archives
296         if ($_0_4 == "PK\x03\x04") {
297             return 'application/zip';   
298         }
299        
300         if ($length > 257) {
301             if (substr($content, 257, 6) == "ustar\x00") {
302                 return 'application/x-tar';   
303             }
304             if (substr($content, 257, 8) == "ustar\x40\x40\x00") {
305                 return 'application/x-tar';   
306             }
307         }
308        
309         if ($_0_4 == 'Rar!') {
310             return 'application/x-rar-compressed';   
311         }
312        
313         if ($_0_2 == "\x1F\x9D") {
314             return 'application/x-compress';   
315         }
316        
317         if ($_0_2 == "\x1F\x8B") {
318             return 'application/x-gzip';   
319         }
320        
321         if ($_0_3 == 'BZh') {
322             return 'application/x-bzip2';   
323         }
324        
325         if ($_0_4 == "SIT!" || $_0_4 == "SITD" || substr($content, 0, 7) == 'StuffIt') {
326             return 'application/x-stuffit';   
327         }   
328        
329        
330         // Text files
331         if (strpos($content, '<?xml') !== FALSE) {
332             if (stripos($content, '<!DOCTYPE') !== FALSE) {
333                 return 'application/xhtml+xml';
334             }
335             if (strpos($content, '<svg') !== FALSE) {
336                 return 'image/svg+xml';
337             }
338             if (strpos($content, '<rss') !== FALSE) {
339                 return 'application/rss+xml';
340             }
341             return 'application/xml';   
342         }   
343        
344         if (strpos($content, '<?php') !== FALSE || strpos($content, '<?=') !== FALSE) {
345             return 'application/x-httpd-php';   
346         }
347        
348         if (preg_match('#^\#\![/a-z0-9]+(python|perl|php|ruby)$#mi', $content, $matches)) {
349             switch (strtolower($matches[1])) {
350                 case 'php':
351                     return 'application/x-httpd-php';
352                 case 'python':
353                     return 'application/x-python';
354                 case 'perl':
355                     return 'application/x-perl';
356                 case 'ruby':
357                     return 'application/x-ruby';
358             }   
359         }
360        
361        
362         // Default
363         return 'application/octet-stream';
364     }
365    
366    
367     /**
368     * Uses the extension of the all-text file to determine the mime type
369     *
370     * @param  string $extension  The file extension
371     * @return string  The mime type of the file
372     */
373     static private function determineMimeTypeByExtension($extension)
374     {
375         switch ($extension) {
376             case 'css':
377                 return 'text/css';
378            
379             case 'csv':
380                 return 'text/csv';
381            
382             case 'htm':
383             case 'html':
384             case 'xhtml':
385                 return 'text/html';
386                
387             case 'ics':
388                 return 'text/calendar';
389            
390             case 'js':
391                 return 'application/javascript';
392            
393             case 'php':
394             case 'php3':
395             case 'php4':
396             case 'php5':
397             case 'inc':
398                 return 'application/x-httpd-php';
399                
400             case 'pl':
401             case 'cgi':
402                 return 'application/x-perl';
403            
404             case 'py':
405                 return 'application/x-python';
406            
407             case 'rb':
408             case 'rhtml':
409                 return 'application/x-ruby';
410            
411             case 'rss':
412                 return 'application/rss+xml';
413                
414             case 'tab':
415                 return 'text/tab-separated-values';
416            
417             case 'vcf':
418                 return 'text/x-vcard';
419            
420             case 'xml':
421                 return 'application/xml';
422            
423             default:
424                 return 'text/plain';   
425         }
426     }
427    
428    
429     /**
430     * The current line of the file
431     *
432     * @var string
433     */
434     private $current_line = NULL;
435    
436     /**
437     * The current line number of the file
438     *
439     * @var string
440     */
441     private $current_line_number = NULL;
442    
443     /**
444     * A backtrace from when the file was deleted
445     *
446     * @var array
447     */
448     protected $deleted = NULL;
449    
450     /**
451     * The full path to the file
452     *
453     * @var string
454     */
455     protected $file;
456    
457     /**
458     * The file handle for iteration
459     *
460     * @var resource
461     */
462     private $file_handle = NULL;
463    
464    
465     /**
466     * Duplicates a file in the current directory when the object is cloned
467     *
468     * @internal
469      *
470     * @return fFile  The new fFile object
471     */
472     public function __clone()
473     {
474         $this->tossIfDeleted();
475        
476         $directory = $this->getParent();
477        
478         if (!$directory->isWritable()) {
479             throw new fEnvironmentException(
480                 'The file count not be cloned because the containing directory, %s, is not writable',
481                 $directory
482             );
483         }
484        
485         $file = fFilesystem::makeUniqueName($directory->getPath() . $this->getName());
486        
487         copy($this->getPath(), $file);
488         chmod($file, fileperms($this->getPath()));
489        
490         $this->file    =& fFilesystem::hookFilenameMap($file);
491         $this->deleted =& fFilesystem::hookDeletedMap($file);
492        
493         // Allow filesystem transactions
494         if (fFilesystem::isInsideTransaction()) {
495             fFilesystem::recordDuplicate($this);
496         }
497     }
498    
499    
500     /**
501     * Creates an object to represent a file on the filesystem
502     *
503     * If multiple fFile objects are created for a single file, they will
504     * reflect changes in each other including rename and delete actions.
505     *
506     * @throws fValidationException  When no file was specified, the file does not exist or the path specified is not a file
507     *
508     * @param  string  $file         The path to the file
509     * @param  boolean $skip_checks  If file checks should be skipped, which improves performance, but may cause undefined behavior - only skip these if they are duplicated elsewhere
510     * @return fFile
511     */
512     public function __construct($file, $skip_checks=FALSE)
513     {
514         if (!$skip_checks) {
515             if (empty($file)) {
516                 throw new fValidationException(
517                     'No filename was specified'
518                 );
519             }
520            
521             if (!is_readable($file)) {
522                 throw new fValidationException(
523                     'The file specified, %s, does not exist or is not readable',
524                     $file
525                 );
526             }
527             if (is_dir($file)) {
528                 throw new fValidationException(
529                     'The file specified, %s, is actually a directory',
530                     $file
531                 );
532             }
533         }
534        
535         // Store the file as an absolute path
536         $file = realpath($file);
537        
538         $this->file    =& fFilesystem::hookFilenameMap($file);
539         $this->deleted =& fFilesystem::hookDeletedMap($file);
540        
541         // If the file is listed as deleted and were not inside a transaction,
542         // but we've gotten to here, then the file exists, so we can wipe the backtrace
543         if ($this->deleted !== NULL && !fFilesystem::isInsideTransaction()) {
544             fFilesystem::updateDeletedMap($file, NULL);
545         }
546     }
547    
548    
549     /**
550     * All requests that hit this method should be requests for callbacks
551     *
552     * @internal
553      *
554     * @param  string $method  The method to create a callback for
555     * @return callback  The callback for the method requested
556     */
557     public function __get($method)
558     {
559         return array($this, $method);       
560     }
561    
562    
563     /**
564     * The iterator information doesn't need to be serialized since a resource can't be
565     *
566     * @internal
567      *
568     * @return array  The instance variables to serialize
569     */
570     public function __sleep()
571     {
572         return array('deleted', 'file');
573     }
574    
575    
576     /**
577     * Returns the filename of the file
578     *
579     * @return string  The filename
580     */
581     public function __toString()
582     {
583         try {
584             return $this->getName();
585         } catch (Exception $e) {
586             return '';   
587         }
588     }
589    
590    
591     /**
592     * Re-inserts the file back into the filesystem map when unserialized
593     *
594     * @internal
595      *
596     * @return void
597     */
598     public function __wakeup()
599     {
600         $file    = $this->file;
601         $deleted = $this->deleted;
602        
603         $this->file    =& fFilesystem::hookFilenameMap($file);
604         $this->deleted =& fFilesystem::hookDeletedMap($file);
605        
606         if ($deleted !== NULL) {
607             fFilesystem::updateDeletedMap($file, $deleted);
608         }
609     }
610    
611    
612     /**
613     * Returns the current line of the file (required by iterator interface)
614     *
615     * @throws fNoRemainingException   When there are no remaining lines in the file
616     * @internal
617      *
618     * @return array  The current row
619     */
620     public function current()
621     {
622         $this->tossIfDeleted();
623        
624         // Primes the result set
625         if ($this->file_handle === NULL) {
626             $this->next();
627            
628         } elseif (!$this->valid()) {
629             throw new fNoRemainingException('There are no remaining lines');
630         }
631        
632         return $this->current_line;
633     }
634    
635    
636     /**
637     * Deletes the current file
638     *
639     * This operation will NOT be performed until the filesystem transaction
640     * has been committed, if a transaction is in progress. Any non-Flourish
641     * code (PHP or system) will still see this file as existing until that
642     * point.
643     *
644     * @return void
645     */
646     public function delete()
647     {
648         if ($this->deleted) {
649             return;
650         }
651        
652         if (!$this->getParent()->isWritable()) {
653             throw new fEnvironmentException(
654                 'The file, %s, can not be deleted because the directory containing it is not writable',
655                 $this->file
656             );
657         }
658        
659         // Allow filesystem transactions
660         if (fFilesystem::isInsideTransaction()) {
661             return fFilesystem::recordDelete($this);
662         }
663        
664         unlink($this->file);
665        
666         fFilesystem::updateDeletedMap($this->file, debug_backtrace());
667         fFilesystem::updateFilenameMap($this->file, '*DELETED at ' . time() . ' with token ' . uniqid('', TRUE) . '* ' . $this->file);
668     }
669    
670    
671     /**
672     * Creates a new file object with a copy of this file
673     *
674     * If no directory is specified, the file is created with a new name in
675     * the current directory. If a new directory is specified, you must also
676     * indicate if you wish to overwrite an existing file with the same name
677     * in the new directory or create a unique name.
678     *
679     * This operation will be reverted by a filesystem transaction being rolled
680     * back.
681     *
682     * @param  string|fDirectory $new_directory  The directory to duplicate the file into if different than the current directory
683     * @param  boolean           $overwrite      If a new directory is specified, this indicates if a file with the same name should be overwritten.
684     * @return fFile  The new fFile object
685     */
686     public function duplicate($new_directory=NULL, $overwrite=NULL)
687     {
688         $this->tossIfDeleted();
689        
690         if ($new_directory === NULL) {
691             $new_directory = $this->getParent();
692         }
693        
694         if (!is_object($new_directory)) {
695             $new_directory = new fDirectory($new_directory);
696         }
697        
698         $new_filename = $new_directory->getPath() . $this->getName();
699        
700         $check_dir_permissions = FALSE;
701        
702         if (file_exists($new_filename)) {
703             if (!$overwrite) {
704                 $new_filename = fFilesystem::makeUniqueName($new_filename);
705                 $check_dir_permissions = TRUE;
706                
707             } elseif (!is_writable($new_filename)) {
708                 throw new fEnvironmentException(
709                     'The new directory specified, %1$s, already contains a file with the name %2$s, but it is not writable',
710                     $new_directory->getPath(),
711                     $this->getName()
712                 );
713             }
714            
715         } else {
716             $check_dir_permissions = TRUE;
717         }
718        
719         if ($check_dir_permissions) {
720             if (!$new_directory->isWritable()) {
721                 throw new fEnvironmentException(
722                     'The new directory specified, %s, is not writable',
723                     $new_directory
724                 );
725             }
726         }
727        
728         copy($this->getPath(), $new_filename);
729         chmod($new_filename, fileperms($this->getPath()));
730        
731         $class = get_class($this);
732         $file  = new $class($new_filename);
733        
734         // Allow filesystem transactions
735         if (fFilesystem::isInsideTransaction()) {
736             fFilesystem::recordDuplicate($file);
737         }
738        
739         return $file;
740     }
741    
742    
743     /**
744     * Gets the file's mime type
745     *
746     * This method will attempt to look at the file contents and the file
747     * extension to determine the mime type. If the file contains binary
748     * information, the contents will be used for mime type verification,
749     * however if the contents appear to be plain text, the file extension
750     * will be used.
751     *
752     * The following mime types are supported. All other binary file types
753     * will be returned as `application/octet-stream` and all other text files
754     * will be returned as `text/plain`.
755     *
756     * **Archive:**
757     *
758     *  - `application/x-bzip2` BZip2 file
759     *  - `application/x-compress` Compress (*nix) file
760     *  - `application/x-gzip` GZip file
761     *  - `application/x-rar-compressed` Rar file
762     *  - `application/x-stuffit` StuffIt file
763     *  - `application/x-tar` Tar file
764     *  - `application/zip` Zip file
765     *
766     * **Audio:**
767     *
768     *  - `audio/x-flac` FLAC audio
769     *  - `audio/mpeg` MP3 audio
770     *  - `audio/mp4` MP4 (AAC) audio
771     *  - `audio/vorbis` Ogg Vorbis audio
772     *  - `audio/x-wav` WAV audio
773     *  - `audio/x-ms-wma` Windows media audio
774     *
775     * **Document:**
776     *
777     *  - `application/vnd.ms-excel` Excel (2000, 2003 and 2007) file
778     *  - `application/pdf` PDF file
779     *  - `application/vnd.ms-powerpoint` Powerpoint (2000, 2003, 2007) file
780     *  - `text/rtf` RTF file
781     *  - `application/msword` Word (2000, 2003 and 2007) file
782     *
783     * **Image:**
784     *
785     *  - `image/x-ms-bmp` BMP file
786     *  - `application/postscript` EPS file
787     *  - `image/gif` GIF file
788     *  - `application/vnd.microsoft.icon` ICO file
789     *  - `image/jpeg` JPEG file
790     *  - `image/png` PNG file
791     *  - `image/tiff` TIFF file
792     *  - `image/svg+xml` SVG file
793     *
794     * **Text:**
795     *
796     *  - `text/css` CSS file
797     *  - `text/csv` CSV file
798     *  - `text/html` (X)HTML file
799     *  - `text/calendar` iCalendar file
800     *  - `application/javascript` Javascript file
801     *  - `application/x-perl` Perl file
802     *  - `application/x-httpd-php` PHP file
803     *  - `application/x-python` Python file
804     *  - `application/rss+xml` RSS feed
805     *  - `application/x-ruby` Ruby file
806     *  - `text/tab-separated-values` TAB file
807     *  - `text/x-vcard` VCard file
808     *  - `application/xhtml+xml` XHTML (Real) file
809     *  - `application/xml` XML file
810     *
811     * **Video/Animation:**
812     *
813     *  - `video/x-msvideo` AVI video
814     *  - `application/x-shockwave-flash` Flash movie
815     *  - `video/x-flv` Flash video
816     *  - `video/x-ms-asf` Microsoft ASF video
817     *  - `video/mp4` MP4 video
818     *  - `video/ogg` OGM and Ogg Theora video
819     *  - `video/quicktime` Quicktime video
820     *  - `video/x-ms-wmv` Windows media video
821     *
822     * @return string  The mime type of the file
823     */
824     public function getMimeType()
825     {
826         $this->tossIfDeleted();
827        
828         return self::determineMimeType($this->file);   
829     }
830    
831    
832     /**
833     * Returns the last modification time of the file
834     *
835     * @return fTimestamp  The timestamp of when the file was last modified
836     */
837     public function getMTime()
838     {
839         $this->tossIfDeleted();
840        
841         return new fTimestamp(filemtime($this->file));   
842     }
843    
844    
845     /**
846     * Gets the filename (i.e. does not include the directory)
847     *
848     * @return string  The filename of the file
849     */
850     public function getName()
851     {
852         // For some reason PHP calls the filename the basename, where filename is the filename minus the extension
853         return fFilesystem::getPathInfo($this->file, 'basename');
854     }
855    
856    
857     /**
858     * Gets the directory the file is located in
859     *
860     * @return fDirectory  The directory containing the file
861     */
862     public function getParent()
863     {
864         return new fDirectory(fFilesystem::getPathInfo($this->file, 'dirname'));
865     }
866    
867    
868     /**
869     * Gets the file's current path (directory and filename)
870     *
871     * If the web path is requested, uses translations set with
872     * fFilesystem::addWebPathTranslation()
873     *
874     * @param  boolean $translate_to_web_path  If the path should be the web path
875     * @return string  The path (directory and filename) for the file
876     */
877     public function getPath($translate_to_web_path=FALSE)
878     {
879         if ($translate_to_web_path) {
880             return fFilesystem::translateToWebPath($this->file);
881         }
882         return $this->file;
883     }
884    
885    
886     /**
887     * Gets the size of the file
888     *
889     * The return value may be incorrect for files over 2GB on 32-bit OSes.
890     *
891     * @param  boolean $format          If the filesize should be formatted for human readability
892     * @param  integer $decimal_places  The number of decimal places to format to (if enabled)
893     * @return integer|string  If formatted a string with filesize in b/kb/mb/gb/tb, otherwise an integer
894     */
895     public function getSize($format=FALSE, $decimal_places=1)
896     {
897         $this->tossIfDeleted();
898        
899         // This technique can overcome signed integer limit
900         $size = sprintf("%u", filesize($this->file));
901        
902         if (!$format) {
903             return $size;
904         }
905        
906         return fFilesystem::formatFilesize($size, $decimal_places);
907     }
908    
909    
910     /**
911     * Check to see if the current file is writable
912     *
913     * @return boolean  If the file is writable
914     */
915     public function isWritable()
916     {
917         $this->tossIfDeleted();
918        
919         return is_writable($this->file);
920     }
921    
922    
923     /**
924     * Returns the current one-based line number (required by iterator interface)
925     *
926     * @throws fNoRemainingException  When there are no remaining lines in the file
927     * @internal
928      *
929     * @return integer  The current line number
930     */
931     public function key()
932     {
933         $this->tossIfDeleted();
934        
935         if ($this->file_handle === NULL) {
936             $this->next();
937            
938         } elseif (!$this->valid()) {
939             throw new fNoRemainingException('There are no remaining lines');
940         }
941        
942         return $this->current_line_number;
943     }
944    
945    
946     /**
947     * Moves the current file to a different directory
948     *
949     * Please note that ::rename() will rename a file in its directory or rename
950     * it into a different directory.
951     *
952     * If the current file's filename already exists in the new directory and
953     * the overwrite flag is set to false, the filename will be changed to a
954     * unique name.
955     *
956     * This operation will be reverted if a filesystem transaction is in
957     * progress and is later rolled back.
958     *
959     * @throws fValidationException  When the directory passed is not a directory or is not readable
960     *
961     * @param  fDirectory|string $new_directory  The directory to move this file into
962     * @param  boolean           $overwrite      If the current filename already exists in the new directory, `TRUE` will cause the file to be overwritten, `FALSE` will cause the new filename to change
963     * @return fFile  The file object, to allow for method chaining
964     */
965     public function move($new_directory, $overwrite)
966     {
967         if (!$new_directory instanceof fDirectory) {
968             $new_directory = new fDirectory($new_directory);
969         }
970        
971         return $this->rename($new_directory->getPath() . $this->getName(), $overwrite);
972     }
973    
974    
975     /**
976     * Advances to the next line in the file (required by iterator interface)
977     *
978     * @throws fNoRemainingException  When there are no remaining lines in the file
979     * @internal
980      *
981     * @return void
982     */
983     public function next()
984     {
985         $this->tossIfDeleted();
986        
987         if ($this->file_handle === NULL) {
988             $this->file_handle         = fopen($this->file, 'r');
989             $this->current_line        = '';
990             $this->current_line_number = 0;
991            
992         } elseif (!$this->valid()) {
993             throw new fNoRemainingException('There are no remaining lines');
994         }
995        
996         $this->current_line = fgets($this->file_handle);
997         $this->current_line_number++;
998     }
999    
1000    
1001     /**
1002     * Prints the contents of the file
1003     *
1004     * This method is primarily intended for when PHP is used to control access
1005     * to files.
1006     *
1007     * Be sure to turn off output buffering and close the session, if open, to
1008     * prevent performance issues.
1009     *
1010     * @param  boolean $headers   If HTTP headers for the file should be included
1011     * @param  mixed   $filename  Present the file as an attachment instead of just outputting type headers - if a string is passed, that will be used for the filename, if `TRUE` is passed, the current filename will be used
1012     * @return fFile  The file object, to allow for method chaining
1013     */
1014     public function output($headers, $filename=NULL)
1015     {
1016         $this->tossIfDeleted();
1017        
1018         if (ob_get_level() > 0) {
1019             throw new fProgrammerException(
1020                 'The method requested, %1$s, can not be used when output buffering is turned on, due to potential memory issues. Please call %2$s, %3$s and %4$s, or %5$s as appropriate to turn off output buffering.',
1021                 'output()',
1022                 'ob_end_clean()',
1023                 'fBuffer::erase()',
1024                 'fBuffer::stop()',
1025                 'fTemplating::destroy()'
1026             );
1027         }
1028        
1029         if ($headers) {
1030             if ($filename !== NULL) {
1031                 if ($filename === TRUE) { $filename = $this->getName();    }
1032                 header('Content-Disposition: attachment; filename="' . $filename . '"');       
1033             }
1034             header('Cache-Control: ');
1035             header('Content-Length: ' . $this->getSize());
1036             header('Content-Type: ' . $this->getMimeType());
1037             header('Expires: ');
1038             header('Last-Modified: ' . $this->getMTime()->format('D, d M Y H:i:s'));
1039             header('Pragma: ');   
1040         }
1041            
1042         readfile($this->file);
1043        
1044         return $this;
1045     }
1046    
1047    
1048     /**
1049     * Reads the data from the file
1050     *
1051     * Reads all file data into memory, use with caution on large files!
1052     *
1053     * This operation will read the data that has been written during the
1054     * current transaction if one is in progress.
1055     *
1056     * @param  mixed $data  The data to write to the file
1057     * @return string  The contents of the file
1058     */
1059     public function read()
1060     {
1061         $this->tossIfDeleted();
1062        
1063         return file_get_contents($this->file);
1064     }
1065    
1066    
1067     /**
1068     * Renames the current file
1069     *
1070     * If the filename already exists and the overwrite flag is set to false,
1071     * a new filename will be created.
1072     *
1073     * This operation will be reverted if a filesystem transaction is in
1074     * progress and is later rolled back.
1075     *
1076     * @param  string  $new_filename  The new full path to the file or a new filename in the current directory
1077     * @param  boolean $overwrite     If the new filename already exists, `TRUE` will cause the file to be overwritten, `FALSE` will cause the new filename to change
1078     * @return fFile  The file object, to allow for method chaining
1079     */
1080     public function rename($new_filename, $overwrite)
1081     {
1082         $this->tossIfDeleted();
1083        
1084         if (!$this->getParent()->isWritable()) {
1085             throw new fEnvironmentException(
1086                 'The file, %s, can not be renamed because the directory containing it is not writable',
1087                 $this->file
1088             );
1089         }
1090        
1091         // If the filename does not contain any folder traversal, rename the file in the current directory
1092         if (preg_match('#^[^/\\\\]+$#D', $new_filename)) {
1093             $new_filename = $this->getParent()->getPath() . $new_filename;       
1094         }
1095        
1096         $info = fFilesystem::getPathInfo($new_filename);
1097        
1098         if (!file_exists($info['dirname'])) {
1099             throw new fProgrammerException(
1100                 'The new filename specified, %s, is inside of a directory that does not exist',
1101                 $new_filename
1102             );
1103         }
1104        
1105         // Make the filename absolute
1106         $new_filename = fDirectory::makeCanonical(realpath($info['dirname'])) . $info['basename'];
1107        
1108         if (file_exists($new_filename)) {
1109             if (!is_writable($new_filename)) {
1110                 throw new fEnvironmentException(
1111                     'The new filename specified, %s, already exists, but is not writable',
1112                     $new_filename
1113                 );
1114             }
1115            
1116             if (!$overwrite) {
1117                 $new_filename = fFilesystem::makeUniqueName($new_filename);
1118            
1119             } else {
1120                 if (fFilesystem::isInsideTransaction()) {
1121                     fFilesystem::recordWrite(new fFile($new_filename));
1122                 }
1123                 // Windows requires that the existing file be deleted before being replaced
1124                 unlink($new_filename);   
1125             }
1126                
1127         } else {
1128             $new_dir = new fDirectory($info['dirname']);
1129             if (!$new_dir->isWritable()) {
1130                 throw new fEnvironmentException(
1131                     'The new filename specified, %s, is inside of a directory that is not writable',
1132                     $new_filename
1133                 );
1134             }
1135         }
1136        
1137         rename($this->file, $new_filename);
1138        
1139         // Allow filesystem transactions
1140         if (fFilesystem::isInsideTransaction()) {
1141             fFilesystem::recordRename($this->file, $new_filename);
1142         }
1143        
1144         fFilesystem::updateFilenameMap($this->file, $new_filename);
1145        
1146         return $this;
1147     }
1148    
1149    
1150     /**
1151     * Rewinds the file handle (required by iterator interface)
1152     *
1153     * @internal
1154      *
1155     * @return void
1156     */
1157     public function rewind()
1158     {
1159         $this->tossIfDeleted();
1160        
1161         if ($this->file_handle !== NULL) {
1162             rewind($this->file_handle);   
1163         }
1164     }
1165    
1166    
1167     /**
1168     * Throws an fProgrammerException if the file has been deleted
1169     *
1170     * @return void
1171     */
1172     protected function tossIfDeleted()
1173     {
1174         if ($this->deleted) {
1175             throw new fProgrammerException(
1176                 "The action requested can not be performed because the file has been deleted\n\nBacktrace for fFile::delete() call:\n%s",
1177                 fCore::backtrace(0, $this->deleted)
1178             );
1179         }
1180     }
1181    
1182    
1183     /**
1184     * Returns if the file has any lines left (required by iterator interface)
1185     *
1186     * @internal
1187      *
1188     * @return boolean  If the iterator is still valid
1189     */
1190     public function valid()
1191     {
1192         $this->tossIfDeleted();
1193        
1194         if ($this->file_handle === NULL) {
1195             return TRUE;
1196         }
1197        
1198         return $this->current_line !== FALSE;
1199     }
1200    
1201    
1202     /**
1203     * Writes the provided data to the file
1204     *
1205     * Requires all previous data to be stored in memory if inside a
1206     * transaction, use with caution on large files!
1207     *
1208     * If a filesystem transaction is in progress and is rolled back, the
1209     * previous data will be restored.
1210     *
1211     * @param  mixed $data  The data to write to the file
1212     * @return fFile  The file object, to allow for method chaining
1213     */
1214     public function write($data)
1215     {
1216         $this->tossIfDeleted();
1217        
1218         if (!$this->isWritable()) {
1219             throw new fEnvironmentException(
1220                 'This file, %s, can not be written to because it is not writable',
1221                 $this->file
1222             );
1223         }
1224        
1225         // Allow filesystem transactions
1226         if (fFilesystem::isInsideTransaction()) {
1227             fFilesystem::recordWrite($this);
1228         }
1229        
1230         file_put_contents($this->file, $data);
1231        
1232         return $this;
1233     }
1234 }
1235  
1236  
1237  
1238 /**
1239  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
1240  *
1241  * Permission is hereby granted, free of charge, to any person obtaining a copy
1242  * of this software and associated documentation files (the "Software"), to deal
1243  * in the Software without restriction, including without limitation the rights
1244  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1245  * copies of the Software, and to permit persons to whom the Software is
1246  * furnished to do so, subject to the following conditions:
1247  *
1248  * The above copyright notice and this permission notice shall be included in
1249  * all copies or substantial portions of the Software.
1250  *
1251  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1252  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1253  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1254  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1255  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1256  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1257  * THE SOFTWARE.
1258  */