]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/upload_file.php
Release 6.5.16
[Github/sugarcrm.git] / include / upload_file.php
1 <?php
2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4  * SugarCRM Community Edition is a customer relationship management program developed by
5  * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
6  * 
7  * This program is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU Affero General Public License version 3 as published by the
9  * Free Software Foundation with the addition of the following permission added
10  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11  * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13  * 
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
17  * details.
18  * 
19  * You should have received a copy of the GNU Affero General Public License along with
20  * this program; if not, see http://www.gnu.org/licenses or write to the Free
21  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22  * 02110-1301 USA.
23  * 
24  * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25  * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
26  * 
27  * The interactive user interfaces in modified source and object code versions
28  * of this program must display Appropriate Legal Notices, as required under
29  * Section 5 of the GNU Affero General Public License version 3.
30  * 
31  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32  * these Appropriate Legal Notices must retain the display of the "Powered by
33  * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34  * technical reasons, the Appropriate Legal Notices must display the words
35  * "Powered by SugarCRM".
36  ********************************************************************************/
37
38 /*********************************************************************************
39
40  * Description:
41  ********************************************************************************/
42 require_once('include/externalAPI/ExternalAPIFactory.php');
43
44 /**
45  * @api
46  * Manage uploaded files
47  */
48 class UploadFile
49 {
50         var $field_name;
51         var $stored_file_name;
52         var $uploaded_file_name;
53         var $original_file_name;
54         var $temp_file_location;
55         var $use_soap = false;
56         var $file;
57         var $file_ext;
58         protected static $url = "upload/";
59
60         /**
61          * Upload errors
62          * @var array
63          */
64         protected static $filesError = array(
65                         UPLOAD_ERR_OK => 'UPLOAD_ERR_OK - There is no error, the file uploaded with success.',
66                         UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE - The uploaded file exceeds the upload_max_filesize directive in php.ini.',
67                         UPLOAD_ERR_FORM_SIZE => 'UPLOAD_ERR_FORM_SIZE - The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
68                         UPLOAD_ERR_PARTIAL => 'UPLOAD_ERR_PARTIAL - The uploaded file was only partially uploaded.',
69                         UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE - No file was uploaded.',
70                         5 => 'UNKNOWN ERROR',
71                         UPLOAD_ERR_NO_TMP_DIR => 'UPLOAD_ERR_NO_TMP_DIR - Missing a temporary folder.',
72                         UPLOAD_ERR_CANT_WRITE => 'UPLOAD_ERR_CANT_WRITE - Failed to write file to disk.',
73                         UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION - A PHP extension stopped the file upload.',
74                         );
75
76         /**
77          * Create upload file handler
78          * @param string $field_name Form field name
79          */
80         function UploadFile ($field_name = '')
81         {
82                 // $field_name is the name of your passed file selector field in your form
83                 // i.e., for Emails, it is "email_attachmentX" where X is 0-9
84                 $this->field_name = $field_name;
85         }
86
87         /**
88          * Setup for SOAP upload
89          * @param string $filename Name for the file
90          * @param string $file
91          */
92         function set_for_soap($filename, $file) {
93                 $this->stored_file_name = $filename;
94                 $this->use_soap = true;
95                 $this->file = $file;
96         }
97
98         /**
99          * Get URL for a document
100          * @deprecated
101          * @param string stored_file_name File name in filesystem
102          * @param string bean_id note bean ID
103          * @return string path with file name
104          */
105         public static function get_url($stored_file_name, $bean_id)
106         {
107                 if ( empty($bean_id) && empty($stored_file_name) ) {
108             return self::$url;
109                 }
110
111                 return self::$url . $bean_id;
112         }
113
114         /**
115          * Get URL of the uploaded file related to the document
116          * @param SugarBean $document
117          * @param string $type Type of the document, if different from $document
118          */
119         public static function get_upload_url($document, $type = null)
120         {
121             if(empty($type)) {
122                 $type = $document->module_dir;
123             }
124             return "index.php?entryPoint=download&type=$type&id={$document->id}";
125         }
126         
127         /**
128          * Try renaming a file to bean_id name
129          * @param string $filename
130          * @param string $bean_id
131          */
132         protected static function tryRename($filename, $bean_id)
133         {
134             $fullname = "upload://$bean_id.$filename";
135             if(file_exists($fullname)) {
136             if(!rename($fullname,  "upload://$bean_id")) {
137                 $GLOBALS['log']->fatal("unable to rename file: $fullname => $bean_id");
138             }
139                 return true;
140             }
141             return false;
142         }
143
144         /**
145          * builds a URL path for an anchor tag
146          * @param string stored_file_name File name in filesystem
147          * @param string bean_id note bean ID
148          * @return string path with file name
149          */
150         static public function get_file_path($stored_file_name, $bean_id, $skip_rename = false)
151         {
152                 global $locale;
153
154         // if the parameters are empty strings, just return back the upload_dir
155                 if ( empty($bean_id) && empty($stored_file_name) ) {
156             return "upload://";
157                 }
158
159                 if(!$skip_rename) {
160                 self::tryRename(rawurlencode($stored_file_name), $bean_id) ||
161                 self::tryRename(urlencode($stored_file_name), $bean_id) ||
162                 self::tryRename($stored_file_name, $bean_id) ||
163                 self::tryRename($locale->translateCharset( $stored_file_name, 'UTF-8', $locale->getExportCharset()), $bean_id);
164                 }
165
166                 return "upload://$bean_id";
167         }
168
169         /**
170          * duplicates an already uploaded file in the filesystem.
171          * @param string old_id ID of original note
172          * @param string new_id ID of new (copied) note
173          * @param string filename Filename of file (deprecated)
174          */
175         public static function duplicate_file($old_id, $new_id, $file_name)
176         {
177                 global $sugar_config;
178
179                 // current file system (GUID)
180                 $source = "upload://$old_id";
181
182                 if(!file_exists($source)) {
183                         // old-style file system (GUID.filename.extension)
184                         $oldStyleSource = $source.$file_name;
185                         if(file_exists($oldStyleSource)) {
186                                 // change to new style
187                                 if(copy($oldStyleSource, $source)) {
188                                         // delete the old
189                                         if(!unlink($oldStyleSource)) {
190                                                 $GLOBALS['log']->error("upload_file could not unlink [ {$oldStyleSource} ]");
191                                         }
192                                 } else {
193                                         $GLOBALS['log']->error("upload_file could not copy [ {$oldStyleSource} ] to [ {$source} ]");
194                                 }
195                         }
196                 }
197
198                 $destination = "upload://$new_id";
199                 if(!copy($source, $destination)) {
200                         $GLOBALS['log']->error("upload_file could not copy [ {$source} ] to [ {$destination} ]");
201                 }
202         }
203
204         /**
205          * Get upload error from system
206          * @return string upload error
207          */
208         public function get_upload_error()
209         {
210             if(isset($this->field_name) && isset($_FILES[$this->field_name]['error'])) {
211                 return $_FILES[$this->field_name]['error'];
212             }
213             return false;
214         }
215
216         /**
217          * standard PHP file-upload security measures. all variables accessed in a global context
218          * @return bool True on success
219          */
220         public function confirm_upload()
221         {
222                 global $sugar_config;
223
224                 if(empty($this->field_name) || !isset($_FILES[$this->field_name])) {
225                     return false;
226                 }
227
228         //check to see if there are any errors from upload
229                 if($_FILES[$this->field_name]['error'] != UPLOAD_ERR_OK) {
230                     if($_FILES[$this->field_name]['error'] != UPLOAD_ERR_NO_FILE) {
231                 if($_FILES[$this->field_name]['error'] == UPLOAD_ERR_INI_SIZE) {
232                     //log the error, the string produced will read something like:
233                     //ERROR: There was an error during upload. Error code: 1 - UPLOAD_ERR_INI_SIZE - The uploaded file exceeds the upload_max_filesize directive in php.ini. upload_maxsize is 16
234                     $errMess = string_format($GLOBALS['app_strings']['UPLOAD_ERROR_TEXT_SIZEINFO'],array($_FILES['filename_file']['error'], self::$filesError[$_FILES['filename_file']['error']],$sugar_config['upload_maxsize']));
235                     $GLOBALS['log']->fatal($errMess);
236                 }else{
237                     //log the error, the string produced will read something like:
238                     //ERROR: There was an error during upload. Error code: 3 - UPLOAD_ERR_PARTIAL - The uploaded file was only partially uploaded.
239                     $errMess = string_format($GLOBALS['app_strings']['UPLOAD_ERROR_TEXT'],array($_FILES['filename_file']['error'], self::$filesError[$_FILES['filename_file']['error']]));
240                     $GLOBALS['log']->fatal($errMess);
241                 }
242                     }
243                     return false;
244                 }
245
246                 if(!is_uploaded_file($_FILES[$this->field_name]['tmp_name'])) {
247                         return false;
248                 } elseif($_FILES[$this->field_name]['size'] > $sugar_config['upload_maxsize']) {
249                     $GLOBALS['log']->fatal("ERROR: uploaded file was too big: max filesize: {$sugar_config['upload_maxsize']}");
250                         return false;
251                 }
252
253                 if(!UploadStream::writable()) {
254                     $GLOBALS['log']->fatal("ERROR: cannot write to upload directory");
255                         return false;
256                 }
257
258                 $this->mime_type = $this->getMime($_FILES[$this->field_name]);
259                 $this->stored_file_name = $this->create_stored_filename();
260                 $this->temp_file_location = $_FILES[$this->field_name]['tmp_name'];
261                 $this->uploaded_file_name = $_FILES[$this->field_name]['name'];
262
263                 return true;
264         }
265
266         /**
267          * Guess MIME type for file
268          * @param string $filename
269          * @return string MIME type
270          */
271         function getMimeSoap($filename){
272
273                 if( function_exists( 'ext2mime' ) )
274                 {
275                         $mime = ext2mime($filename);
276                 }
277                 else
278                 {
279                         $mime = ' application/octet-stream';
280                 }
281                 return $mime;
282
283         }
284
285         /**
286          * Get MIME type for uploaded file
287          * @param array $_FILES_element $_FILES element required
288          * @return string MIME type
289          */
290         function getMime($_FILES_element)
291         {
292                 $filename = $_FILES_element['name'];
293         $filetype = isset($_FILES_element['type']) ? $_FILES_element['type'] : null;
294         $file_ext = pathinfo($filename, PATHINFO_EXTENSION);
295
296         $is_image = strpos($filetype, 'image/') === 0;
297         // if it's an image, or no file extension is available and the mime is octet-stream
298         // try to determine the mime type
299         $recheckMime = $is_image || (empty($file_ext) && $filetype == 'application/octet-stream');
300
301         $mime = 'application/octet-stream';
302         if ($filetype && !$recheckMime) {
303             $mime = $filetype;
304                 } elseif( function_exists( 'mime_content_type' ) ) {
305                         $mime = mime_content_type( $_FILES_element['tmp_name'] );
306         } elseif ($is_image) {
307             $info = getimagesize($_FILES_element['tmp_name']);
308             if ($info) {
309                 $mime = $info['mime'];
310             }
311         } elseif (function_exists('ext2mime')) {
312             $mime = ext2mime($filename);
313         }
314
315         return $mime;
316         }
317
318         /**
319          * gets note's filename
320          * @return string
321          */
322         function get_stored_file_name()
323         {
324                 return $this->stored_file_name;
325         }
326         
327         function get_temp_file_location()
328         {
329             return $this->temp_file_location;
330         }
331         
332         function get_uploaded_file_name()
333         {
334             return $this->uploaded_file_name;
335         }
336         
337         function get_mime_type()
338         {
339             return $this->mime_type;
340         }
341         
342         /**
343          * Returns the contents of the uploaded file
344          */
345         public function get_file_contents() {
346             
347             // Need to call
348             if ( !isset($this->temp_file_location) ) {
349                 $this->confirm_upload();
350             }
351             
352             if (($data = @file_get_contents($this->temp_file_location)) === false) {
353                 return false;
354         }
355            
356         return $data;
357         }
358
359         
360
361         /**
362          * creates a file's name for preparation for saving
363          * @return string
364          */
365         function create_stored_filename()
366         {
367                 global $sugar_config;
368
369                 if(!$this->use_soap) {
370                         $stored_file_name = $_FILES[$this->field_name]['name'];
371                         $this->original_file_name = $stored_file_name;
372
373                         /**
374                          * cn: bug 8056 - windows filesystems and IIS do not like utf8.  we are forced to urlencode() to ensure that
375                          * the file is linkable from the browser.  this will stay broken until we move to a db-storage system
376                          */
377                         if(is_windows()) {
378                                 // create a non UTF-8 name encoding
379                                 // 176 + 36 char guid = windows' maximum filename length
380                                 $end = (strlen($stored_file_name) > 176) ? 176 : strlen($stored_file_name);
381                                 $stored_file_name = substr($stored_file_name, 0, $end);
382                                 $this->original_file_name = $_FILES[$this->field_name]['name'];
383                         }
384                     $stored_file_name = str_replace("\\", "", $stored_file_name);
385                 } else {
386                         $stored_file_name = $this->stored_file_name;
387                         $this->original_file_name = $stored_file_name;
388                 }
389
390                 $this->file_ext = pathinfo($stored_file_name, PATHINFO_EXTENSION);
391         // cn: bug 6347 - fix file extension detection
392         foreach($sugar_config['upload_badext'] as $badExt) {
393             if(strtolower($this->file_ext) == strtolower($badExt)) {
394                 $stored_file_name .= ".txt";
395                 $this->file_ext="txt";
396                 break; // no need to look for more
397             }
398         }
399                 return $stored_file_name;
400         }
401
402         /**
403          * moves uploaded temp file to permanent save location
404          * @param string bean_id ID of parent bean
405          * @return bool True on success
406          */
407         function final_move($bean_id)
408         {
409             $destination = $bean_id;
410             if(substr($destination, 0, 9) != "upload://") {
411             $destination = "upload://$bean_id";
412             }
413         if($this->use_soap) {
414                 if(!file_put_contents($destination, $this->file)){
415                     $GLOBALS['log']->fatal("ERROR: can't save file to $destination");
416                 return false;
417                 }
418                 } else {
419                         if(!UploadStream::move_uploaded_file($_FILES[$this->field_name]['tmp_name'], $destination)) {
420                             $GLOBALS['log']->fatal("ERROR: can't move_uploaded_file to $destination. You should try making the directory writable by the webserver");
421                 return false;
422                         }
423                 }
424                 return true;
425         }
426
427         /**
428          * Upload document to external service
429          * @param SugarBean $bean Related bean
430          * @param string $bean_id
431          * @param string $doc_type
432          * @param string $file_name
433          * @param string $mime_type
434          */
435         function upload_doc($bean, $bean_id, $doc_type, $file_name, $mime_type)
436         {
437                 if(!empty($doc_type)&&$doc_type!='Sugar') {
438                         global $sugar_config;
439                 $destination = $this->get_upload_path($bean_id);
440                 sugar_rename($destination, str_replace($bean_id, $bean_id.'_'.$file_name, $destination));
441                 $new_destination = $this->get_upload_path($bean_id.'_'.$file_name);
442
443                     try{
444                 $this->api = ExternalAPIFactory::loadAPI($doc_type);
445
446                 if ( isset($this->api) && $this->api !== false ) {
447                     $result = $this->api->uploadDoc(
448                         $bean,
449                         $new_destination,
450                         $file_name,
451                         $mime_type
452                         );
453                 } else {
454                     $result['success'] = FALSE;
455                     // FIXME: Translate
456                     $GLOBALS['log']->error("Could not load the requested API (".$doc_type.")");
457                     $result['errorMessage'] = 'Could not find a proper API';
458                 }
459             }catch(Exception $e){
460                 $result['success'] = FALSE;
461                 $result['errorMessage'] = $e->getMessage();
462                 $GLOBALS['log']->error("Caught exception: (".$e->getMessage().") ");
463             }
464             if ( !$result['success'] ) {
465                 sugar_rename($new_destination, str_replace($bean_id.'_'.$file_name, $bean_id, $new_destination));
466                 $bean->doc_type = 'Sugar';
467                 // FIXME: Translate
468                 if ( ! is_array($_SESSION['user_error_message']) )
469                     $_SESSION['user_error_message'] = array();
470
471                 $error_message = isset($result['errorMessage']) ? $result['errorMessage'] : $GLOBALS['app_strings']['ERR_EXTERNAL_API_SAVE_FAIL'];
472                 $_SESSION['user_error_message'][] = $error_message;
473
474             }
475             else {
476                 unlink($new_destination);
477             }
478         }
479
480         }
481
482         /**
483          * returns the path with file name to save an uploaded file
484          * @param string bean_id ID of the parent bean
485          * @return string
486          */
487         function get_upload_path($bean_id)
488         {
489                 $file_name = $bean_id;
490
491                 // cn: bug 8056 - mbcs filename in urlencoding > 212 chars in Windows fails
492                 $end = (strlen($file_name) > 212) ? 212 : strlen($file_name);
493                 $ret_file_name = substr($file_name, 0, $end);
494
495                 return "upload://$ret_file_name";
496         }
497
498         /**
499          * deletes a file
500          * @param string bean_id ID of the parent bean
501          * @param string file_name File's name
502          */
503         static public function unlink_file($bean_id,$file_name = '')
504         {
505             if(file_exists("upload://$bean_id$file_name")) {
506             return unlink("upload://$bean_id$file_name");
507             }
508     }
509
510     /**
511      * Get upload file location prefix
512      * @return string prefix
513      */
514     public function get_upload_dir()
515     {
516         return "upload://";
517     }
518
519     /**
520      * Return real FS path of the file
521      * @param string $path
522      */
523     public static function realpath($path)
524     {
525        if(substr($path, 0, 9) == "upload://") {
526            $path = UploadStream::path($path);
527        }
528        $ret = realpath($path);
529        return $ret?$ret:$path;
530     }
531
532     /**
533      * Return path of uploaded file relative to uploads dir
534      * @param string $path
535      */
536     public static function relativeName($path)
537     {
538         if(substr($path, 0, 9) == "upload://") {
539             $path = substr($path, 9);
540         }
541         return $path;
542     }
543 }
544
545 /**
546  * @internal
547  * Upload file stream handler
548  */
549 class UploadStream
550 {
551     const STREAM_NAME = "upload";
552     protected static $upload_dir;
553
554     /**
555      * Method checks Suhosin restrictions to use streams in php
556      *
557      * @static
558      * @return bool is allowed stream or not
559      */
560     public static function getSuhosinStatus()
561     {
562         // looks like suhosin patch doesn't block protocols, only suhosin extension (tested on FreeBSD)
563         // if suhosin is not installed it is okay for us
564         if (extension_loaded('suhosin') == false)
565         {
566             return true;
567         }
568         $configuration = ini_get_all('suhosin', false);
569
570         // suhosin simulation is okay for us
571         if ($configuration['suhosin.simulation'] == true)
572         {
573             return true;
574         }
575
576         // checking that UploadStream::STREAM_NAME is allowed by white list
577         $streams = $configuration['suhosin.executor.include.whitelist'];
578         if ($streams != '')
579         {
580             $streams = explode(',', $streams);
581             foreach($streams as $stream)
582             {
583                 $stream = explode('://', $stream, 2);
584                 if (count($stream) == 1)
585                 {
586                     if ($stream[0] == UploadStream::STREAM_NAME)
587                     {
588                         return true;
589                     }
590                 }
591                 elseif ($stream[1] == '' && $stream[0] == UploadStream::STREAM_NAME)
592                 {
593                     return true;
594                 }
595             }
596
597             $GLOBALS['log']->fatal('Stream ' . UploadStream::STREAM_NAME . ' is not listed in suhosin.executor.include.whitelist and blocked because of it');
598             return false;
599         }
600
601         // checking that UploadStream::STREAM_NAME is not blocked by black list
602         $streams = $configuration['suhosin.executor.include.blacklist'];
603         if ($streams != '')
604         {
605             $streams = explode(',', $streams);
606             foreach($streams as $stream)
607             {
608                 $stream = explode('://', $stream, 2);
609                 if ($stream[0] == UploadStream::STREAM_NAME)
610                 {
611                     $GLOBALS['log']->fatal('Stream ' . UploadStream::STREAM_NAME . 'is listed in suhosin.executor.include.blacklist and blocked because of it');
612                     return false;
613                 }
614             }
615             return true;
616         }
617
618         $GLOBALS['log']->fatal('Suhosin blocks all streams, please define ' . UploadStream::STREAM_NAME . ' stream in suhosin.executor.include.whitelist');
619         return false;
620     }
621
622     /**
623      * Get upload directory
624      * @return string
625      */
626     public static function getDir()
627     {
628         if(empty(self::$upload_dir)) {
629             self::$upload_dir = rtrim($GLOBALS['sugar_config']['upload_dir'], '/\\');
630             if(empty(self::$upload_dir)) {
631                 self::$upload_dir = "upload";
632             }
633             if(!file_exists(self::$upload_dir)) {
634                 sugar_mkdir(self::$upload_dir, 0755, true);
635             }
636         }
637         return self::$upload_dir;
638     }
639
640     /**
641      * Check if upload dir is writable
642      * @return bool
643      */
644     public static function writable()
645     {
646         return is_writable(self::getDir());
647     }
648
649     /**
650      * Register the stream
651      */
652     public function register()
653     {
654         stream_register_wrapper(self::STREAM_NAME, __CLASS__);
655     }
656
657     /**
658      * Get real FS path of the upload stream file
659      * @param string $path Upload stream path (with upload://)
660      * @return string FS path
661      */
662     public static function path($path)
663     {
664         $path = substr($path, strlen(self::STREAM_NAME)+3); // cut off upload://
665         $path = str_replace("\\", "/", $path); // canonicalize path
666         if($path == ".." || substr($path, 0, 3) == "../" || substr($path, -3, 3) == "/.." || strstr($path, "/../")) {
667                 return null;
668         }
669         return self::getDir()."/".$path;
670     }
671
672     /**
673      * Ensure upload subdir exists
674      * @param string $path Upload stream path (with upload://)
675      * @param bool $writable
676      * @return boolean
677      */
678     public static function ensureDir($path, $writable = true)
679     {
680         $path = self::path($path);
681         if(!is_dir($path)) {
682            return sugar_mkdir($path, 0755, true);
683         }
684         return true;
685     }
686
687     public function dir_closedir()
688     {
689         closedir($this->dirp);
690     }
691
692     public function dir_opendir ($path, $options )
693     {
694         $this->dirp = opendir(self::path($path));
695         return !empty($this->dirp);
696     }
697
698     public function dir_readdir()
699     {
700         return readdir($this->dirp);
701     }
702
703     public function dir_rewinddir()
704     {
705         return rewinddir($this->dirp);
706     }
707
708     public function mkdir($path, $mode, $options)
709     {
710         return mkdir(self::path($path), $mode, ($options&STREAM_MKDIR_RECURSIVE) != 0);
711     }
712
713     public function rename($path_from, $path_to)
714     {
715         return rename(self::path($path_from), self::path($path_to));
716     }
717
718     public function rmdir($path, $options)
719     {
720         return rmdir(self::path($path));
721     }
722
723     public function stream_cast ($cast_as)
724     {
725         return $this->fp;
726     }
727
728     public function stream_close ()
729     {
730         fclose($this->fp);
731         return true;
732     }
733
734     public function stream_eof ()
735     {
736         return feof($this->fp);
737     }
738    public function stream_flush ()
739     {
740         return fflush($this->fp);
741     }
742
743     public function stream_lock($operation)
744     {
745         return flock($this->fp, $operation);
746     }
747
748     public function stream_open($path, $mode)
749     {
750         $fullpath = self::path($path);
751         if(empty($fullpath)) return false;
752         if($mode == 'r') {
753             $this->fp = fopen($fullpath, $mode);
754         } else {
755             // if we will be writing, try to transparently create the directory
756             $this->fp = @fopen($fullpath, $mode);
757             if(!$this->fp && !file_exists(dirname($fullpath))) {
758                 mkdir(dirname($fullpath), 0755, true);
759                 $this->fp = fopen($fullpath, $mode);
760             }
761         }
762         return !empty($this->fp);
763     }
764
765     public function stream_read($count)
766     {
767         return fread($this->fp, $count);
768     }
769
770     public function stream_seek($offset, $whence = SEEK_SET)
771     {
772         return fseek($this->fp, $offset, $whence) == 0;
773     }
774
775     public function stream_set_option($option, $arg1, $arg2)
776     {
777         return true;
778     }
779
780     public function stream_stat()
781     {
782         return fstat($this->fp);
783     }
784
785     public function stream_tell()
786     {
787         return ftell($this->fp);
788     }
789     public function stream_write($data)
790     {
791         return fwrite($this->fp, $data);
792     }
793
794     public function unlink($path)
795     {
796         unlink(self::path($path));
797         return true;
798     }
799
800     public function url_stat($path, $flags)
801     {
802         return @stat(self::path($path));
803     }
804
805     public static function move_uploaded_file($upload, $path)
806     {
807         return move_uploaded_file($upload, self::path($path));
808     }
809 }
810