root/fORMFile.php

Revision 833, 42.6 kB (checked in by wbond, 2 months ago)

BackwardsCompatibilityBreak - fActiveRecord::validate() now uses column names as array keys if messages are returned, the $validation_messages parameter for the pre::validate() and post::validate() hooks now requires array keys be column names, fValidation::validate() now uses field names as array keys if messages are returned, fUpload::validate() no longer returns a $_FILES array. fValidation::addRequiredFields() no longer accepts one-or-more rules, instead use fValidation::addOneOrMoreRule(). fValidation::addRequiredFields() no longer accepts conditional rules, instead use fValidation::addConditionalRule().

Fixed ticket #226 with fValidation::overrideFieldName(), fValidation::addStringReplacement() and fValidation::addRegexReplacement(). Fixed ticket #145 with fValidation::addRegexRule(). Fixed ticket #298 with fValidation::addFileUploadRule(). Fixed ticket #373 with $return_messages param combined with new $remove_field_names/$remote_column_names params in fValidation::validate() and fORMValidation::validate().

Added fORMValidation::addRegexRule(), fValidationException::removeFieldNames() and lots of new fValidation functionality. Fixed a bug with checking mimetype of text files with fUpload.

LineHide Line Numbers
1 <?php
2 /**
3  * Provides file manipulation functionality for fActiveRecord classes
4  *
5  * @copyright  Copyright (c) 2008-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fORMFile
11  *
12  * @version    1.0.0b24
13  * @changes    1.0.0b24  Changed validation messages array to use column name keys [wb, 2010-05-26]
14  * @changes    1.0.0b23  Fixed a bug with ::upload() that could cause a method called on a non-object error in relation to the upload directory not being defined [wb, 2010-05-10]
15  * @changes    1.0.0b22  Updated the TEMP_DIRECTORY constant to not include the trailing slash, code now uses DIRECTORY_SEPARATOR to fix issues on Windows [wb, 2010-04-28]
16  * @changes    1.0.0b21  Fixed ::set() to perform column inheritance, just like ::upload() does [wb, 2010-03-15]
17  * @changes    1.0.0b20  Fixed the `set` and `process` methods to return the record instance, changed `upload` methods to return the fFile object, updated ::reflect() with new return values [wb, 2010-03-15]
18  * @changes    1.0.0b19  Fixed a few missed instances of old fFile method names [wb, 2009-12-16]
19  * @changes    1.0.0b18  Updated code for the new fFile API [wb, 2009-12-16]
20  * @changes    1.0.0b17  Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
21  * @changes    1.0.0b16  fImage method calls for file upload columns will no longer cause notices due to a missing image type [wb, 2009-09-09]
22  * @changes    1.0.0b15  ::addFImageMethodCall() no longer requires column be an image upload column, inheritance to an image column now only happens for fImage objects [wb, 2009-07-29]
23  * @changes    1.0.0b14  Updated to use new fORM::registerInspectCallback() method [wb, 2009-07-13]
24  * @changes    1.0.0b13  Updated code for new fORM API [wb, 2009-06-15]
25  * @changes    1.0.0b12  Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
26  * @changes    1.0.0b11  Updated code to use new fValidationException::formatField() method [wb, 2009-06-04] 
27  * @changes    1.0.0b10  Fixed a bug where an inherited file upload column would not be properly re-set with an `existing-` input [wb, 2009-05-26]
28  * @changes    1.0.0b9   ::upload() and ::set() now set the `$values` entry to `NULL` for filenames that are empty [wb, 2009-03-02]
29  * @changes    1.0.0b8   Changed ::set() to accept objects and reject directories [wb, 2009-01-21]
30  * @changes    1.0.0b7   Changed the class to use the new fFilesystem::createObject() method [wb, 2009-01-21]
31  * @changes    1.0.0b6   Old files are now checked against the current file to prevent removal of an in-use file [wb, 2008-12-23]
32  * @changes    1.0.0b5   Fixed ::replicate() to ensure the temp directory exists and ::set() to use the temp directory [wb, 2008-12-23]
33  * @changes    1.0.0b4   ::objectify() no longer throws an exception when a file can't be found [wb, 2008-12-18]
34  * @changes    1.0.0b3   Added ::replicate() so that replicated files get pu in the temp directory [wb, 2008-12-12]
35  * @changes    1.0.0b2   Fixed a bug with objectifying file columns [wb, 2008-11-24]
36  * @changes    1.0.0b    The initial implementation [wb, 2008-05-28]
37  */
38 class fORMFile
39 {
40     // The following constants allow for nice looking callbacks to static methods
41     const addFImageMethodCall        = 'fORMFile::addFImageMethodCall';
42     const addFUploadMethodCall       = 'fORMFile::addFUploadMethodCall';
43     const begin                      = 'fORMFile::begin';
44     const commit                     = 'fORMFile::commit';
45     const configureColumnInheritance = 'fORMFile::configureColumnInheritance';
46     const configureFileUploadColumn  = 'fORMFile::configureFileUploadColumn';
47     const configureImageUploadColumn = 'fORMFile::configureImageUploadColumn';
48     const delete                     = 'fORMFile::delete';
49     const deleteOld                  = 'fORMFile::deleteOld';
50     const encode                     = 'fORMFile::encode';
51     const inspect                    = 'fORMFile::inspect';
52     const moveFromTemp               = 'fORMFile::moveFromTemp';
53     const objectify                  = 'fORMFile::objectify';
54     const populate                   = 'fORMFile::populate';
55     const prepare                    = 'fORMFile::prepare';
56     const process                    = 'fORMFile::process';
57     const processImage               = 'fORMFile::processImage';
58     const reflect                    = 'fORMFile::reflect';
59     const replicate                  = 'fORMFile::replicate';
60     const reset                      = 'fORMFile::reset';
61     const rollback                   = 'fORMFile::rollback';
62     const set                        = 'fORMFile::set';
63     const upload                     = 'fORMFile::upload';
64     const validate                   = 'fORMFile::validate';
65    
66    
67     /**
68     * The temporary directory to use for various tasks
69     *
70     * @internal
71      *
72     * @var string
73     */
74     const TEMP_DIRECTORY = '__flourish_temp';
75    
76    
77     /**
78     * Defines how columns can inherit uploaded files
79     *
80     * @var array
81     */
82     static private $column_inheritence = array();
83    
84     /**
85     * Methods to be called on fUpload before the file is uploaded
86     *
87     * @var array
88     */
89     static private $fupload_method_calls = array();
90    
91     /**
92     * Columns that can be filled by file uploads
93     *
94     * @var array
95     */
96     static private $file_upload_columns = array();
97    
98     /**
99     * Methods to be called on the fImage instance
100     *
101     * @var array
102     */
103     static private $fimage_method_calls = array();
104    
105     /**
106     * Columns that can be filled by image uploads
107     *
108     * @var array
109     */
110     static private $image_upload_columns = array();
111    
112     /**
113     * Keeps track of the nesting level of the filesystem transaction so we know when to start, commit, rollback, etc
114     *
115     * @var integer
116     */
117     static private $transaction_level = 0;
118    
119    
120     /**
121     * Adds an fImage method call to the image manipulation for a column if an image file is uploaded
122     *
123     * @param  mixed  $class       The class name or instance of the class
124     * @param  string $column      The column to call the method for
125     * @param  string $method      The fImage method to call
126     * @param  array  $parameters  The parameters to pass to the method
127     * @return void
128     */
129     static public function addFImageMethodCall($class, $column, $method, $parameters=array())
130     {
131         $class = fORM::getClass($class);
132        
133         if (empty(self::$file_upload_columns[$class][$column])) {
134             throw new fProgrammerException(
135                 'The column specified, %s, has not been configured as a file or image upload column',
136                 $column
137             );
138         }
139        
140         if (empty(self::$fimage_method_calls[$class])) {
141             self::$fimage_method_calls[$class] = array();
142         }
143         if (empty(self::$fimage_method_calls[$class][$column])) {
144             self::$fimage_method_calls[$class][$column] = array();
145         }
146        
147         self::$fimage_method_calls[$class][$column][] = array(
148             'method'     => $method,
149             'parameters' => $parameters
150         );
151     }
152    
153    
154     /**
155     * Adds an fUpload method call to the fUpload initialization for a column
156     *
157     * @param  mixed  $class       The class name or instance of the class
158     * @param  string $column      The column to call the method for
159     * @param  string $method      The fUpload method to call
160     * @param  array  $parameters  The parameters to pass to the method
161     * @return void
162     */
163     static public function addFUploadMethodCall($class, $column, $method, $parameters=array())
164     {
165         if ($method == 'enableOverwrite') {
166             throw new fProgrammerException(
167                 'The method specified, %1$s, is not compatible with how %2$s stores and associates files with records',
168                 $method,
169                 'fORMFile'
170             );     
171         }
172        
173         $class = fORM::getClass($class);
174        
175         if (empty(self::$file_upload_columns[$class][$column])) {
176             throw new fProgrammerException(
177                 'The column specified, %s, has not been configured as a file or image upload column',
178                 $column
179             );
180         }
181        
182         if (empty(self::$fupload_method_calls[$class])) {
183             self::$fupload_method_calls[$class] = array();
184         }
185         if (empty(self::$fupload_method_calls[$class][$column])) {
186             self::$fupload_method_calls[$class][$column] = array();
187         }
188        
189         self::$fupload_method_calls[$class][$column][] = array(
190             'method'     => $method,
191             'parameters' => $parameters
192         );
193     }
194    
195    
196     /**
197     * Begins a transaction, or increases the level
198     *
199     * @internal
200      *
201     * @return void
202     */
203     static public function begin()
204     {
205         // If the transaction was started by something else, don't even track it
206         if (self::$transaction_level == 0 && fFilesystem::isInsideTransaction()) {
207             return;
208         }
209        
210         self::$transaction_level++;
211        
212         if (!fFilesystem::isInsideTransaction()) {
213             fFilesystem::begin();
214         }
215     }
216    
217    
218     /**
219     * Commits a transaction, or decreases the level
220     *
221     * @internal
222      *
223     * @return void
224     */
225     static public function commit()
226     {
227         // If the transaction was started by something else, don't even track it
228         if (self::$transaction_level == 0) {
229             return;
230         }
231        
232         self::$transaction_level--;
233        
234         if (!self::$transaction_level) {
235             fFilesystem::commit();
236         }
237     }
238    
239    
240     /**
241     * Composes text using fText if loaded
242     *
243     * @param  string  $message    The message to compose
244     * @param  mixed   $component  A string or number to insert into the message
245     * @param  mixed   ...
246     * @return string  The composed and possible translated message
247     */
248     static private function compose($message)
249     {
250         $args = array_slice(func_get_args(), 1);
251        
252         if (class_exists('fText', FALSE)) {
253             return call_user_func_array(
254                 array('fText', 'compose'),
255                 array($message, $args)
256             );
257         } else {
258             return vsprintf($message, $args);
259         }
260     }
261    
262    
263     /**
264     * Sets a column to be a file upload column
265     *
266     * Configuring a column to be a file upload column means that whenever
267     * fActiveRecord::populate() is called for an fActiveRecord object, any
268     * appropriately named file uploads (via `$_FILES`) will be moved into
269     * the directory for this column.
270     *
271     * Setting the column to a file path will cause the specified file to
272     * be copied into the directory for this column.
273     *
274     * @param  mixed             $class      The class name or instance of the class
275     * @param  string            $column     The column to set as a file upload column
276     * @param  fDirectory|string $directory  The directory to upload/move to
277     * @return void
278     */
279     static public function configureFileUploadColumn($class, $column, $directory)
280     {
281         $class     = fORM::getClass($class);
282         $table     = fORM::tablize($class);
283         $schema    = fORMSchema::retrieve($class);
284         $data_type = $schema->getColumnInfo($table, $column, 'type');
285        
286         $valid_data_types = array('varchar', 'char', 'text');
287         if (!in_array($data_type, $valid_data_types)) {
288             throw new fProgrammerException(
289                 'The column specified, %1$s, is a %2$s column. Must be one of %3$s to be set as a file upload column.',
290                 $column,
291                 $data_type,
292                 join(', ', $valid_data_types)
293             );
294         }
295        
296         if (!is_object($directory)) {
297             $directory = new fDirectory($directory);
298         }
299        
300         if (!$directory->isWritable()) {
301             throw new fEnvironmentException(
302                 'The file upload directory, %s, is not writable',
303                 $directory->getPath()
304             );
305         }
306        
307         $camelized_column = fGrammar::camelize($column, TRUE);
308        
309         fORM::registerActiveRecordMethod(
310             $class,
311             'upload' . $camelized_column,
312             self::upload
313         );
314        
315         fORM::registerActiveRecordMethod(
316             $class,
317             'set' . $camelized_column,
318             self::set
319         );
320        
321         fORM::registerActiveRecordMethod(
322             $class,
323             'encode' . $camelized_column,
324             self::encode
325         );
326        
327         fORM::registerActiveRecordMethod(
328             $class,
329             'prepare' . $camelized_column,
330             self::prepare
331         );
332        
333         fORM::registerReflectCallback($class, self::reflect);
334         fORM::registerInspectCallback($class, $column, self::inspect);
335         fORM::registerReplicateCallback($class, $column, self::replicate);
336         fORM::registerObjectifyCallback($class, $column, self::objectify);
337        
338         $only_once_hooks = array(
339             'post-begin::delete()'    => self::begin,
340             'pre-commit::delete()'    => self::delete,
341             'post-commit::delete()'   => self::commit,
342             'post-rollback::delete()' => self::rollback,
343             'post::populate()'        => self::populate,
344             'post-begin::store()'     => self::begin,
345             'post-validate::store()'  => self::moveFromTemp,
346             'pre-commit::store()'     => self::deleteOld,
347             'post-commit::store()'    => self::commit,
348             'post-rollback::store()'  => self::rollback,
349             'post::validate()'        => self::validate
350         );
351        
352         foreach ($only_once_hooks as $hook => $callback) {
353             if (!fORM::checkHookCallback($class, $hook, $callback)) {
354                 fORM::registerHookCallback($class, $hook, $callback);
355             }
356         }
357        
358         if (empty(self::$file_upload_columns[$class])) {
359             self::$file_upload_columns[$class] = array();
360         }
361        
362         self::$file_upload_columns[$class][$column] = $directory;
363     }
364    
365    
366     /**
367     * Takes one file or image upload columns and sets it to inherit any uploaded/set files from another column
368     *
369     * @param  mixed  $class                The class name or instance of the class
370     * @param  string $column               The column that will inherit the uploaded file
371     * @param  string $inherit_from_column  The column to inherit the uploaded file from
372     * @return void
373     */
374     static public function configureColumnInheritance($class, $column, $inherit_from_column)
375     {
376         $class = fORM::getClass($class);
377        
378         if (empty(self::$column_inheritence[$class])) {
379             self::$column_inheritence[$class] = array();
380         }
381        
382         if (empty(self::$column_inheritence[$class][$inherit_from_column])) {
383             self::$column_inheritence[$class][$inherit_from_column] = array();
384         }
385        
386         self::$column_inheritence[$class][$inherit_from_column][] = $column;
387     }
388    
389    
390     /**
391     * Sets a column to be an image upload column
392     *
393     * This method works exactly the same as ::configureFileUploadColumn()
394     * except that only image files are accepted.
395     *
396     * @param  mixed             $class       The class name or instance of the class
397     * @param  string            $column      The column to set as a file upload column
398     * @param  fDirectory|string $directory   The directory to upload to
399     * @param  string            $image_type  The image type to save the image as: `NULL`, `'gif'`, `'jpg'`, `'png'`
400     * @return void
401     */
402     static public function configureImageUploadColumn($class, $column, $directory, $image_type=NULL)
403     {
404         $valid_image_types = array(NULL, 'gif', 'jpg', 'png');
405         if (!in_array($image_type, $valid_image_types)) {
406             $valid_image_types[0] = '{null}';
407             throw new fProgrammerException(
408                 'The image type specified, %1$s, is not valid. Must be one of: %2$s.',
409                 $image_type,
410                 join(', ', $valid_image_types)
411             );
412         }
413        
414         self::configureFileUploadColumn($class, $column, $directory);
415        
416         $class = fORM::getClass($class);
417        
418         $camelized_column = fGrammar::camelize($column, TRUE);
419        
420         fORM::registerActiveRecordMethod(
421             $class,
422             'process' . $camelized_column,
423             self::process
424         );
425        
426         if (empty(self::$image_upload_columns[$class])) {
427             self::$image_upload_columns[$class] = array();
428         }
429        
430         self::$image_upload_columns[$class][$column] = $image_type;
431        
432         self::addFUploadMethodCall(
433             $class,
434             $column,
435             'setMimeTypes',
436             array(
437                 array(
438                     'image/gif',
439                     'image/jpeg',
440                     'image/pjpeg',
441                     'image/png'
442                 ),
443                 self::compose('The file uploaded is not an image')
444             )
445         );
446     }
447    
448    
449     /**
450     * Deletes the files for this record
451     *
452     * @internal
453      *
454     * @param  fActiveRecord $object            The fActiveRecord instance
455     * @param  array         &$values           The current values
456     * @param  array         &$old_values       The old values
457     * @param  array         &$related_records  Any records related to this record
458     * @param  array         &$cache            The cache array for the record
459     * @return void
460     */
461     static public function delete($object, &$values, &$old_values, &$related_records, &$cache)
462     {
463         $class = get_class($object);
464        
465         foreach (self::$file_upload_columns[$class] as $column => $directory) {
466            
467             // Remove the current file for the column
468             if ($values[$column] instanceof fFile) {
469                 $values[$column]->delete();
470             }
471            
472             // Remove the old files for the column
473             foreach (fActiveRecord::retrieveOld($old_values, $column, array(), TRUE) as $file) {
474                 if ($file instanceof fFile) {
475                     $file->delete();
476                 }
477             }
478         }
479     }
480    
481    
482     /**
483     * Deletes old files for this record that have been replaced by new ones
484     *
485     * @internal
486      *
487     * @param  fActiveRecord $object            The fActiveRecord instance
488     * @param  array         &$values           The current values
489     * @param  array         &$old_values       The old values
490     * @param  array         &$related_records  Any records related to this record
491     * @param  array         &$cache            The cache array for the record
492     * @return void
493     */
494     static public function deleteOld($object, &$values, &$old_values, &$related_records, &$cache)
495     {
496         $class = get_class($object);
497        
498         // Remove the old files for the column
499         foreach (self::$file_upload_columns[$class] as $column => $directory) {
500             $current_file = $values[$column];
501             foreach (fActiveRecord::retrieveOld($old_values, $column, array(), TRUE) as $file) {
502                 if ($file instanceof fFile && (!$current_file instanceof fFile || $current_file->getPath() != $file->getPath())) {
503                     $file->delete();
504                 }
505             }
506         }
507     }
508    
509    
510     /**
511     * Encodes a file for output into an HTML `input` tag
512     *
513     * @internal
514      *
515     * @param  fActiveRecord $object            The fActiveRecord instance
516     * @param  array         &$values           The current values
517     * @param  array         &$old_values       The old values
518     * @param  array         &$related_records  Any records related to this record
519     * @param  array         &$cache            The cache array for the record
520     * @param  string        $method_name       The method that was called
521     * @param  array         $parameters        The parameters passed to the method
522     * @return void
523     */
524     static public function encode($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
525     {
526         list ($action, $column) = fORM::parseMethod($method_name);
527        
528         $filename = ($values[$column] instanceof fFile) ? $values[$column]->getName() : NULL;
529         if ($filename && strpos($values[$column]->getPath(), self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR . $filename) !== FALSE) {
530             $filename = self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR . $filename;
531         }
532        
533         return fHTML::encode($filename);
534     }
535    
536    
537     /**
538     * Adds metadata about features added by this class
539     *
540     * @internal
541      *
542     * @param  string $class      The class being inspected
543     * @param  string $column     The column being inspected
544     * @param  array  &$metadata  The array of metadata about a column
545     * @return void
546     */
547     static public function inspect($class, $column, &$metadata)
548     {
549         if (!empty(self::$image_upload_columns[$class][$column])) {
550             $metadata['feature'] = 'image';
551            
552         } elseif (!empty(self::$file_upload_columns[$class][$column])) {
553             $metadata['feature'] = 'file';
554         }
555        
556         $metadata['directory'] = self::$file_upload_columns[$class][$column]->getPath();
557     }
558    
559    
560     /**
561     * Moves uploaded files from the temporary directory to the permanent directory
562     *
563     * @internal
564      *
565     * @param  fActiveRecord $object            The fActiveRecord instance
566     * @param  array         &$values           The current values
567     * @param  array         &$old_values       The old values
568     * @param  array         &$related_records  Any records related to this record
569     * @param  array         &$cache            The cache array for the record
570     * @return void
571     */
572     static public function moveFromTemp($object, &$values, &$old_values, &$related_records, &$cache)
573     {
574         foreach ($values as $column => $value) {
575             if (!$value instanceof fFile) {
576                 continue;
577             }
578            
579             // If the file is in a temp dir, move it out
580             if (strpos($value->getParent()->getPath(), self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR) !== FALSE) {
581                 $new_filename = str_replace(self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR, '', $value->getPath());
582                 $new_filename = fFilesystem::makeUniqueName($new_filename);
583                 $value->rename($new_filename, FALSE);
584             }
585         }
586     }
587    
588    
589     /**
590     * Turns a filename into an fFile or fImage object
591     *
592     * @internal
593      *
594     * @param  string $class   The class this value is for
595     * @param  string $column  The column the value is in
596     * @param  mixed  $value   The value
597     * @return mixed  The fFile, fImage or raw value
598     */
599     static public function objectify($class, $column, $value)
600     {
601         if ((!is_string($value) && !is_numeric($value) && !is_object($value)) || !strlen(trim($value))) {
602             return $value;
603         }
604        
605         $path = self::$file_upload_columns[$class][$column]->getPath() . $value;
606        
607         try {
608            
609             return fFilesystem::createObject($path);
610              
611         // If there was some error creating the file, just return the raw value
612         } catch (fExpectedException $e) {
613             return $value;
614         }
615     }
616    
617    
618     /**
619     * Performs the upload action for file uploads during fActiveRecord::populate()
620     *
621     * @internal
622      *
623     * @param  fActiveRecord $object            The fActiveRecord instance
624     * @param  array         &$values           The current values
625     * @param  array         &$old_values       The old values
626     * @param  array         &$related_records  Any records related to this record
627     * @param  array         &$cache            The cache array for the record
628     * @return void
629     */
630     static public function populate($object, &$values, &$old_values, &$related_records, &$cache)
631     {
632         $class = get_class($object);
633        
634         foreach (self::$file_upload_columns[$class] as $column => $directory) {
635             if (fUpload::check($column) || fRequest::check('existing-' . $column) || fRequest::check('delete-' . $column)) {
636                 $method = 'upload' . fGrammar::camelize($column, TRUE);
637                 $object->$method();
638             }
639         }
640     }
641    
642    
643     /**
644     * Prepares a file for output into HTML by returning filename or the web server path to the file
645     *
646     * @internal
647      *
648     * @param  fActiveRecord $object            The fActiveRecord instance
649     * @param  array         &$values           The current values
650     * @param  array         &$old_values       The old values
651     * @param  array         &$related_records  Any records related to this record
652     * @param  array         &$cache            The cache array for the record
653     * @param  string        $method_name       The method that was called
654     * @param  array         $parameters        The parameters passed to the method
655     * @return void
656     */
657     static public function prepare($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
658     {
659         list ($action, $column) = fORM::parseMethod($method_name);
660        
661         if (sizeof($parameters) > 1) {
662             throw new fProgrammerException(
663                 'The column specified, %s, does not accept more than one parameter',
664                 $column
665             );
666         }
667        
668         $translate_to_web_path = (empty($parameters[0])) ? FALSE : TRUE;
669         $value                 = $values[$column];
670        
671         if ($value instanceof fFile) {
672             $path = ($translate_to_web_path) ? $value->getPath(TRUE) : $value->getName();
673         } else {
674             $path = NULL;
675         }
676        
677         return fHTML::prepare($path);
678     }
679    
680    
681     /**
682     * Takes a directory and creates a temporary directory inside of it - if the temporary folder exists, all files older than 6 hours will be deleted
683     *
684     * @param  string $folder  The folder to create a temporary directory inside of
685     * @return fDirectory  The temporary directory for the folder specified
686     */
687     static private function prepareTempDir($folder)
688     {
689         // Let's clean out the upload temp dir
690         try {
691             $temp_dir = new fDirectory($folder->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
692         } catch (fValidationException $e) {
693             $temp_dir = fDirectory::create($folder->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
694         }
695        
696         $temp_files = $temp_dir->scan();
697         foreach ($temp_files as $temp_file) {
698             if (filemtime($temp_file->getPath()) < strtotime('-6 hours')) {
699                 unlink($temp_file->getPath());
700             }
701         }
702        
703         return $temp_dir;   
704     }
705    
706    
707     /**
708     * Handles re-processing an existing image file
709     *
710     * @internal
711      *
712     * @param  fActiveRecord $object            The fActiveRecord instance
713     * @param  array         &$values           The current values
714     * @param  array         &$old_values       The old values
715     * @param  array         &$related_records  Any records related to this record
716     * @param  array         &$cache            The cache array for the record
717     * @param  string        $method_name       The method that was called
718     * @param  array         $parameters        The parameters passed to the method
719     * @return fActiveRecord  The record object, to allow for method chaining
720     */
721     static public function process($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
722     {
723         list ($action, $column) = fORM::parseMethod($method_name);
724        
725         $class = get_class($object);
726        
727         self::processImage($class, $column, $values[$column]);
728        
729         return $object;
730     }
731    
732    
733     /**
734     * Performs image manipulation on an uploaded/set image
735     *
736     * @internal
737      *
738     * @param  string $class   The name of the class we are manipulating the image for
739     * @param  string $column  The column the image is assigned to
740     * @param  fFile  $image   The image object to manipulate
741     * @return void
742     */
743     static public function processImage($class, $column, $image)
744     {
745         // If we don't have an image or we haven't set it up to manipulate images, just exit
746         if (!$image instanceof fImage || empty(self::$fimage_method_calls[$class][$column])) {
747             return;
748         }
749        
750         // Manipulate the image
751         if (!empty(self::$fimage_method_calls[$class][$column])) {
752             foreach (self::$fimage_method_calls[$class][$column] as $method_call) {
753                 $callback   = array($image, $method_call['method']);
754                 $parameters = $method_call['parameters'];
755                 if (!is_callable($callback)) {
756                     throw new fProgrammerException(
757                         'The fImage method specified, %s, is not a valid method',
758                         $method_call['method'] . '()'
759                     );
760                 }
761                 call_user_func_array($callback, $parameters);
762             }
763         }
764        
765         // Save the changes
766         call_user_func(
767             array($image, 'saveChanges'),
768             isset(self::$image_upload_columns[$class][$column]) ? self::$image_upload_columns[$class][$column] : NULL
769         );
770     }
771    
772    
773     /**
774     * Adjusts the fActiveRecord::reflect() signatures of columns that have been configured in this class
775     *
776     * @internal
777      *
778     * @param  string  $class                 The class to reflect
779     * @param  array   &$signatures           The associative array of `{method name} => {signature}`
780     * @param  boolean $include_doc_comments  If doc comments should be included with the signature
781     * @return void
782     */
783     static public function reflect($class, &$signatures, $include_doc_comments)
784     {
785         $image_columns = (isset(self::$image_upload_columns[$class])) ? array_keys(self::$image_upload_columns[$class]) : array();
786         $file_columns  = (isset(self::$file_upload_columns[$class]))  ? array_keys(self::$file_upload_columns[$class])  : array();
787        
788         foreach($file_columns as $column) {
789             $camelized_column = fGrammar::camelize($column, TRUE);
790            
791             $noun = 'file';
792             if (in_array($column, $image_columns)) {
793                 $noun = 'image';
794             }
795            
796             $signature = '';
797             if ($include_doc_comments) {
798                 $signature .= "/**\n";
799                 $signature .= " * Encodes the filename of " . $column . " for output into an HTML form\n";
800                 $signature .= " * \n";
801                 $signature .= " * Only the filename will be returned, any directory will be stripped.\n";
802                 $signature .= " * \n";
803                 $signature .= " * @return string  The HTML form-ready value\n";
804                 $signature .= " */\n";
805             }
806             $encode_method = 'encode' . $camelized_column;
807             $signature .= 'public function ' . $encode_method . '()';
808            
809             $signatures[$encode_method] = $signature;
810            
811             if (in_array($column, $image_columns)) {
812                 $signature = '';
813                 if ($include_doc_comments) {
814                     $signature .= "/**\n";
815                     $signature .= " * Takes the existing image and runs it through the prescribed fImage method calls\n";
816                     $signature .= " * \n";
817                     $signature .= " * @return fActiveRecord  The record object, to allow for method chaining\n";
818                     $signature .= " */\n";
819                 }
820                 $process_method = 'process' . $camelized_column;
821                 $signature .= 'public function ' . $process_method . '()';
822                
823                 $signatures[$process_method] = $signature;
824             }
825            
826             $signature = '';
827             if ($include_doc_comments) {
828                 $signature .= "/**\n";
829                 $signature .= " * Prepares the filename of " . $column . " for output into HTML\n";
830                 $signature .= " * \n";
831                 $signature .= " * By default only the filename will be returned and any directory will be stripped.\n";
832                 $signature .= " * The \$include_web_path parameter changes this behaviour.\n";
833                 $signature .= " * \n";
834                 $signature .= " * @param  boolean \$include_web_path  If the full web path to the " . $noun . " should be included\n";
835                 $signature .= " * @return string  The HTML-ready value\n";
836                 $signature .= " */\n";
837             }
838             $prepare_method = 'prepare' . $camelized_column;
839             $signature .= 'public function ' . $prepare_method . '($include_web_path=FALSE)';
840            
841             $signatures[$prepare_method] = $signature;
842            
843             $signature = '';
844             if ($include_doc_comments) {
845                 $signature .= "/**\n";
846                 $signature .= " * Takes a file uploaded through an HTML form for " . $column . " and moves it into the specified directory\n";
847                 $signature .= " * \n";
848                 $signature .= " * Any columns that were designated as inheriting from this column will get a copy\n";
849                 $signature .= " * of the uploaded file.\n";
850                 $signature .= " * \n";
851                 if ($noun == 'image') {
852                     $signature .= " * Any fImage calls that were added to the column will be processed on the uploaded image.\n";
853                     $signature .= " * \n";
854                 }
855                 $signature .= " * @return fFile  The uploaded file\n";
856                 $signature .= " */\n";
857             }
858             $upload_method = 'upload' . $camelized_column;
859             $signature .= 'public function ' . $upload_method . '()';
860            
861             $signatures[$upload_method] = $signature;
862            
863             $signature = '';
864             if ($include_doc_comments) {
865                 $signature .= "/**\n";
866                 $signature .= " * Takes a file that exists on the filesystem and copies it into the specified directory for " . $column . "\n";
867                 $signature .= " * \n";
868                 if ($noun == 'image') {
869                     $signature .= " * Any fImage calls that were added to the column will be processed on the copied image.\n";
870                     $signature .= " * \n";
871                 }
872                 $signature .= " * @return fActiveRecord  The record object, to allow for method chaining\n";
873                 $signature .= " */\n";
874             }
875             $set_method = 'set' . $camelized_column;
876             $signature .= 'public function ' . $set_method . '()';
877            
878             $signatures[$set_method] = $signature;
879            
880             $signature = '';
881             if ($include_doc_comments) {
882                 $signature .= "/**\n";
883                 $signature .= " * Returns metadata about " . $column . "\n";
884                 $signature .= " * \n";
885                 $signature .= " * @param  string \$element  The element to return. Must be one of: 'type', 'not_null', 'default', 'valid_values', 'max_length', 'feature', 'directory'.\n";
886                 $signature .= " * @return mixed  The metadata array or a single element\n";
887                 $signature .= " */\n";
888             }
889             $inspect_method = 'inspect' . $camelized_column;
890             $signature .= 'public function ' . $inspect_method . '($element=NULL)';
891            
892             $signatures[$inspect_method] = $signature;
893         }
894     }
895    
896    
897     /**
898     * Creates a copy of an uploaded file in the temp directory for the newly cloned record
899     *
900     * @internal
901      *
902     * @param  string $class   The class this value is for
903     * @param  string $column  The column the value is in
904     * @param  mixed  $value   The value
905     * @return mixed  The cloned fFile object
906     */
907     static public function replicate($class, $column, $value)
908     {
909         if (!$value instanceof fFile) {
910             return $value;   
911         }
912        
913         // If the file we are replicating is in the temp dir, the copy can live there too
914         if (strpos($value->getParent()->getPath(), self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR) !== FALSE) {
915             $value = clone $value;   
916        
917         // Otherwise, the copy of the file must be placed in the temp dir so it is properly cleaned up
918         } else {
919             $upload_dir = self::$file_upload_columns[$class][$column];
920            
921             try {
922                 $temp_dir = new fDirectory($upload_dir->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
923             } catch (fValidationException $e) {
924                 $temp_dir = fDirectory::create($upload_dir->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
925             }
926            
927             $value = $value->duplicate($temp_dir);   
928         }
929        
930         return $value;
931     }
932    
933    
934     /**
935     * Resets the configuration of the class
936     *
937     * @internal
938      *
939     * @return void
940     */
941     static public function reset()
942     {
943         self::$column_inheritence   = array();
944         self::$fupload_method_calls = array();
945         self::$file_upload_columns  = array();
946         self::$fimage_method_calls  = array();
947         self::$image_upload_columns = array();
948         self::$transaction_level    = 0;
949     }
950    
951    
952     /**
953     * Rolls back a transaction, or decreases the level
954     *
955     * @internal
956      *
957     * @return void
958     */
959     static public function rollback()
960     {
961         // If the transaction was started by something else, don't even track it
962         if (self::$transaction_level == 0) {
963             return;
964         }
965        
966         self::$transaction_level--;
967        
968         if (!self::$transaction_level) {
969             fFilesystem::rollback();
970         }
971     }
972    
973    
974     /**
975     * Copies a file from the filesystem to the file upload directory and sets it as the file for the specified column
976     *
977     * This method will perform the fImage calls defined for the column.
978     *
979     * @internal
980      *
981     * @param  fActiveRecord $object            The fActiveRecord instance
982     * @param  array         &$values           The current values
983     * @param  array         &$old_values       The old values
984     * @param  array         &$related_records  Any records related to this record
985     * @param  array         &$cache            The cache array for the record
986     * @param  string        $method_name       The method that was called
987     * @param  array         $parameters        The parameters passed to the method
988     * @return fActiveRecord  The record object, to allow for method chaining
989     */
990     static public function set($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
991     {
992         $class = get_class($object);
993        
994         list ($action, $column) = fORM::parseMethod($method_name);
995        
996         $doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
997        
998         if (!array_key_exists(0, $parameters)) {
999             throw new fProgrammerException(
1000                 'The method %s requires exactly one parameter',
1001                 $method_name . '()'
1002             );
1003         }
1004        
1005         $file_path = $parameters[0];
1006        
1007         // Handle objects being passed in
1008         if ($file_path instanceof fFile) {
1009             $file_path = $file_path->getPath();   
1010         } elseif (is_object($file_path) && is_callable(array($file_path, '__toString'))) {
1011             $file_path = $file_path->__toString();
1012         } elseif (is_object($file_path)) {
1013             $file_path = (string) $file_path;
1014         }
1015        
1016         if ($file_path !== NULL && $file_path !== '' && $file_path !== FALSE) {
1017             if (!$file_path || (!file_exists($file_path) && !file_exists($doc_root . $file_path))) {
1018                 throw new fEnvironmentException(
1019                     'The file specified, %s, does not exist. This may indicate a missing enctype="multipart/form-data" attribute in form tag.',
1020                     $file_path
1021                 );
1022             }
1023            
1024             if (!file_exists($file_path) && file_exists($doc_root . $file_path)) {
1025                 $file_path = $doc_root . $file_path;
1026             }
1027            
1028             if (is_dir($file_path)) {
1029                 throw new fProgrammerException(
1030                     'The file specified, %s, is not a file but a directory',
1031                     $file_path
1032                 );
1033             }
1034            
1035             $upload_dir = self::$file_upload_columns[$class][$column];
1036            
1037             try {
1038                 $temp_dir = new fDirectory($upload_dir->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
1039             } catch (fValidationException $e) {
1040                 $temp_dir = fDirectory::create($upload_dir->getPath() . self::TEMP_DIRECTORY . DIRECTORY_SEPARATOR);
1041             }
1042            
1043             $file     = fFilesystem::createObject($file_path);
1044             $new_file = $file->duplicate($temp_dir);
1045            
1046         } else {
1047             $new_file = NULL;
1048         }
1049        
1050         fActiveRecord::assign($values, $old_values, $column, $new_file);
1051        
1052         // Perform column inheritance
1053         if (!empty(self::$column_inheritence[$class][$column])) {
1054             foreach (self::$column_inheritence[$class][$column] as $other_column) {
1055                 self::set($object, $values, $old_values, $related_records, $cache, 'set' . fGrammar::camelize($other_column, TRUE), array($file));
1056             }
1057         }
1058        
1059         if ($new_file) {
1060             self::processImage($class, $column, $new_file);
1061         }
1062        
1063         return $object;
1064     }
1065    
1066    
1067     /**
1068     * Sets up an fUpload object for a specific column
1069     *
1070     * @param  string $class   The class to set up for
1071     * @param  string $column  The column to set up for
1072     * @return fUpload  The configured fUpload object
1073     */
1074     static private function setUpFUpload($class, $column)
1075     {
1076         $upload = new fUpload();
1077        
1078         // Set up the fUpload class
1079         if (!empty(self::$fupload_method_calls[$class][$column])) {
1080             foreach (self::$fupload_method_calls[$class][$column] as $method_call) {
1081                 if (!is_callable($upload->{$method_call['method']})) {
1082                     throw new fProgrammerException(
1083                         'The fUpload method specified, %s, is not a valid method',
1084                         $method_call['method'] . '()'
1085                     );
1086                 }
1087                 call_user_func_array($upload->{$method_call['method']}, $method_call['parameters']);
1088             }
1089         }
1090        
1091         return $upload;
1092     }
1093    
1094    
1095     /**
1096     * Uploads a file
1097     *
1098     * @internal
1099      *
1100     * @param  fActiveRecord $object            The fActiveRecord instance
1101     * @param  array         &$values           The current values
1102     * @param  array         &$old_values       The old values
1103     * @param  array         &$related_records  Any records related to this record
1104     * @param  array         &$cache            The cache array for the record
1105     * @param  string        $method_name       The method that was called
1106     * @param  array         $parameters        The parameters passed to the method
1107     * @return fFile  The uploaded file
1108     */
1109     static public function upload($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
1110     {
1111         $class = get_class($object);
1112        
1113         list ($action, $column) = fORM::parseMethod($method_name);
1114        
1115         $existing_temp_file = FALSE;
1116        
1117        
1118         // Try to upload the file putting it in the temp dir incase there is a validation problem with the record
1119         try {
1120             $upload_dir = self::$file_upload_columns[$class][$column];
1121             $temp_dir   = self::prepareTempDir($upload_dir);
1122            
1123             if (!fUpload::check($column)) {
1124                 throw new fExpectedException('Please upload a file');   
1125             }
1126            
1127             $uploader = self::setUpFUpload($class, $column);
1128             $file     = $uploader->move($temp_dir, $column);
1129            
1130         // If there was an eror, check to see if we have an existing file
1131         } catch (fExpectedException $e) {
1132            
1133             // If there is an existing file and none was uploaded, substitute the existing file
1134             $existing_file = fRequest::get('existing-' . $column);
1135             $delete_file   = fRequest::get('delete-' . $column, 'boolean');
1136             $no_upload     = $e->getMessage() == self::compose('Please upload a file');
1137            
1138             if ($existing_file && $delete_file && $no_upload) {
1139                 $file = NULL;
1140                
1141             } elseif ($existing_file) {
1142                
1143                 $file_path = $upload_dir->getPath() . $existing_file;
1144                 $file      = fFilesystem::createObject($file_path);
1145                
1146                 $current_file = $values[$column];
1147                
1148                 // If the existing file is the same as the current file, we can just exit now
1149                 if ($current_file && $file->getPath() == $current_file->getPath()) {
1150                     return;   
1151                 }
1152                
1153                 $existing_temp_file = TRUE;
1154                
1155             } else {
1156                 $file = NULL;
1157             }
1158         }
1159        
1160         // Assign the file
1161         fActiveRecord::assign($values, $old_values, $column, $file);
1162        
1163         // Perform the file upload inheritance
1164         if (!empty(self::$column_inheritence[$class][$column])) {
1165             foreach (self::$column_inheritence[$class][$column] as $other_column) {
1166                
1167                 if ($file) {
1168                    
1169                     // Image columns will only inherit if it is an fImage object
1170                     if (!$file instanceof fImage && isset(self::$image_upload_columns[$class][$other_column])) {
1171                         continue;       
1172                     }
1173                    
1174                     $other_upload_dir = self::$file_upload_columns[$class][$other_column];
1175                     $other_temp_dir   = self::prepareTempDir($other_upload_dir);
1176                    
1177                     if ($existing_temp_file) {
1178                         $other_file = fFilesystem::createObject($other_temp_dir->getPath() . $file->getName());
1179                     } else {
1180                         $other_file = $file->duplicate($other_temp_dir, FALSE);
1181                     }
1182                    
1183                 } else {
1184                     $other_file = $file;
1185                 }
1186                
1187                 fActiveRecord::assign($values, $old_values, $other_column, $other_file);
1188                
1189                 if (!$existing_temp_file && $other_file) {
1190                     self::processImage($class, $other_column, $other_file);
1191                 }
1192             }
1193         }
1194        
1195         // Process the file
1196         if (!$existing_temp_file && $file) {
1197             self::processImage($class, $column, $file);
1198         }
1199        
1200         return $file;
1201     }
1202    
1203    
1204     /**
1205     * Validates uploaded files to ensure they match all of the criteria defined
1206     *
1207     * @internal
1208      *
1209     * @param  fActiveRecord $object                The fActiveRecord instance
1210     * @param  array         &$values               The current values
1211     * @param  array         &$old_values           The old values
1212     * @param  array         &$related_records      Any records related to this record
1213     * @param  array         &$cache                The cache array for the record
1214     * @param  array         &$validation_messages  The existing validation messages
1215     * @return void
1216     */
1217     static public function validate($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages)
1218     {
1219         $class = get_class($object);
1220        
1221         foreach (self::$file_upload_columns[$class] as $column => $directory) {
1222             $column_name = fORM::getColumnName($class, $column);
1223            
1224             if (isset($validation_messages[$column])) {
1225                 $search_message  = self::compose('%sPlease enter a value', fValidationException::formatField($column_name));
1226                 $replace_message = self::compose('%sPlease upload a file', fValidationException::formatField($column_name));
1227                 $validation_messages[$column] = str_replace($search_message, $replace_message, $validation_messages[$column]);
1228             }
1229            
1230             // Grab the error that occured
1231             try {
1232                 if (fUpload::check($column)) {
1233                     $uploader = self::setUpFUpload($class, $column);
1234                     $uploader->validate($column);
1235                 }
1236             } catch (fValidationException $e) {
1237                 if ($e->getMessage() != self::compose('Please upload a file')) {
1238                     $validation_messages[$column] = fValidationException::formatField($column_name) . $e->getMessage();
1239                 }
1240             }
1241         }
1242     }
1243    
1244    
1245     /**
1246     * Forces use as a static class
1247     *
1248     * @return fORMFile
1249     */
1250     private function __construct() { }
1251 }
1252  
1253  
1254  
1255 /**
1256  * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
1257  *
1258  * Permission is hereby granted, free of charge, to any person obtaining a copy
1259  * of this software and associated documentation files (the "Software"), to deal
1260  * in the Software without restriction, including without limitation the rights
1261  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1262  * copies of the Software, and to permit persons to whom the Software is
1263  * furnished to do so, subject to the following conditions:
1264  *
1265  * The above copyright notice and this permission notice shall be included in
1266  * all copies or substantial portions of the Software.
1267  *
1268  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1269  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1270  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1271  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1272  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1273  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1274  * THE SOFTWARE.
1275  */