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.
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.
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
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
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.
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.
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 ********************************************************************************/
38 /*********************************************************************************
41 ********************************************************************************/
42 require_once('include/externalAPI/ExternalAPIFactory.php');
46 * Manage uploaded files
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;
58 protected static $url = "upload/";
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.',
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.',
77 * Create upload file handler
78 * @param string $field_name Form field name
80 function UploadFile ($field_name = '')
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;
88 * Setup for SOAP upload
89 * @param string $filename Name for the file
92 function set_for_soap($filename, $file) {
93 $this->stored_file_name = $filename;
94 $this->use_soap = true;
99 * Get URL for a document
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
105 public static function get_url($stored_file_name, $bean_id)
107 if ( empty($bean_id) && empty($stored_file_name) ) {
111 return self::$url . $bean_id;
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
119 public static function get_upload_url($document, $type = null)
122 $type = $document->module_dir;
124 return "index.php?entryPoint=download&type=$type&id={$document->id}";
128 * Try renaming a file to bean_id name
129 * @param string $filename
130 * @param string $bean_id
132 protected static function tryRename($filename, $bean_id)
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");
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
150 static public function get_file_path($stored_file_name, $bean_id, $skip_rename = false)
154 // if the parameters are empty strings, just return back the upload_dir
155 if ( empty($bean_id) && empty($stored_file_name) ) {
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);
166 return "upload://$bean_id";
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)
175 public static function duplicate_file($old_id, $new_id, $file_name)
177 global $sugar_config;
179 // current file system (GUID)
180 $source = "upload://$old_id";
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)) {
189 if(!unlink($oldStyleSource)) {
190 $GLOBALS['log']->error("upload_file could not unlink [ {$oldStyleSource} ]");
193 $GLOBALS['log']->error("upload_file could not copy [ {$oldStyleSource} ] to [ {$source} ]");
198 $destination = "upload://$new_id";
199 if(!copy($source, $destination)) {
200 $GLOBALS['log']->error("upload_file could not copy [ {$source} ] to [ {$destination} ]");
205 * Get upload error from system
206 * @return string upload error
208 public function get_upload_error()
210 if(isset($this->field_name) && isset($_FILES[$this->field_name]['error'])) {
211 return $_FILES[$this->field_name]['error'];
217 * standard PHP file-upload security measures. all variables accessed in a global context
218 * @return bool True on success
220 public function confirm_upload()
222 global $sugar_config;
224 if(empty($this->field_name) || !isset($_FILES[$this->field_name])) {
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);
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);
246 if(!is_uploaded_file($_FILES[$this->field_name]['tmp_name'])) {
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']}");
253 if(!UploadStream::writable()) {
254 $GLOBALS['log']->fatal("ERROR: cannot write to upload directory");
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'];
267 * Guess MIME type for file
268 * @param string $filename
269 * @return string MIME type
271 function getMimeSoap($filename){
273 if( function_exists( 'ext2mime' ) )
275 $mime = ext2mime($filename);
279 $mime = ' application/octet-stream';
286 * Get MIME type for uploaded file
287 * @param array $_FILES_element $_FILES element required
288 * @return string MIME type
290 function getMime($_FILES_element)
292 $filename = $_FILES_element['name'];
293 $file_ext = pathinfo($filename, PATHINFO_EXTENSION);
295 //If no file extension is available and the mime is octet-stream try to determine the mime type.
296 $recheckMime = empty($file_ext) && !empty($_FILES_element['type']) && ($_FILES_element['type'] == 'application/octet-stream');
298 if (!empty($_FILES_element['type']) && !$recheckMime) {
299 $mime = $_FILES_element['type'];
300 } elseif( function_exists( 'mime_content_type' ) ) {
301 $mime = mime_content_type( $_FILES_element['tmp_name'] );
302 } elseif( function_exists( 'ext2mime' ) ) {
303 $mime = ext2mime( $_FILES_element['name'] );
305 $mime = 'application/octet-stream';
311 * gets note's filename
314 function get_stored_file_name()
316 return $this->stored_file_name;
319 function get_temp_file_location()
321 return $this->temp_file_location;
324 function get_uploaded_file_name()
326 return $this->uploaded_file_name;
329 function get_mime_type()
331 return $this->mime_type;
335 * Returns the contents of the uploaded file
337 public function get_file_contents() {
340 if ( !isset($this->temp_file_location) ) {
341 $this->confirm_upload();
344 if (($data = @file_get_contents($this->temp_file_location)) === false) {
354 * creates a file's name for preparation for saving
357 function create_stored_filename()
359 global $sugar_config;
361 if(!$this->use_soap) {
362 $stored_file_name = $_FILES[$this->field_name]['name'];
363 $this->original_file_name = $stored_file_name;
366 * cn: bug 8056 - windows filesystems and IIS do not like utf8. we are forced to urlencode() to ensure that
367 * the file is linkable from the browser. this will stay broken until we move to a db-storage system
370 // create a non UTF-8 name encoding
371 // 176 + 36 char guid = windows' maximum filename length
372 $end = (strlen($stored_file_name) > 176) ? 176 : strlen($stored_file_name);
373 $stored_file_name = substr($stored_file_name, 0, $end);
374 $this->original_file_name = $_FILES[$this->field_name]['name'];
376 $stored_file_name = str_replace("\\", "", $stored_file_name);
378 $stored_file_name = $this->stored_file_name;
379 $this->original_file_name = $stored_file_name;
382 $this->file_ext = pathinfo($stored_file_name, PATHINFO_EXTENSION);
383 // cn: bug 6347 - fix file extension detection
384 foreach($sugar_config['upload_badext'] as $badExt) {
385 if(strtolower($this->file_ext) == strtolower($badExt)) {
386 $stored_file_name .= ".txt";
387 $this->file_ext="txt";
388 break; // no need to look for more
391 return $stored_file_name;
395 * moves uploaded temp file to permanent save location
396 * @param string bean_id ID of parent bean
397 * @return bool True on success
399 function final_move($bean_id)
401 $destination = $bean_id;
402 if(substr($destination, 0, 9) != "upload://") {
403 $destination = "upload://$bean_id";
405 if($this->use_soap) {
406 if(!file_put_contents($destination, $this->file)){
407 $GLOBALS['log']->fatal("ERROR: can't save file to $destination");
411 if(!UploadStream::move_uploaded_file($_FILES[$this->field_name]['tmp_name'], $destination)) {
412 $GLOBALS['log']->fatal("ERROR: can't move_uploaded_file to $destination. You should try making the directory writable by the webserver");
420 * Upload document to external service
421 * @param SugarBean $bean Related bean
422 * @param string $bean_id
423 * @param string $doc_type
424 * @param string $file_name
425 * @param string $mime_type
427 function upload_doc($bean, $bean_id, $doc_type, $file_name, $mime_type)
429 if(!empty($doc_type)&&$doc_type!='Sugar') {
430 global $sugar_config;
431 $destination = $this->get_upload_path($bean_id);
432 sugar_rename($destination, str_replace($bean_id, $bean_id.'_'.$file_name, $destination));
433 $new_destination = $this->get_upload_path($bean_id.'_'.$file_name);
436 $this->api = ExternalAPIFactory::loadAPI($doc_type);
438 if ( isset($this->api) && $this->api !== false ) {
439 $result = $this->api->uploadDoc(
446 $result['success'] = FALSE;
448 $GLOBALS['log']->error("Could not load the requested API (".$doc_type.")");
449 $result['errorMessage'] = 'Could not find a proper API';
451 }catch(Exception $e){
452 $result['success'] = FALSE;
453 $result['errorMessage'] = $e->getMessage();
454 $GLOBALS['log']->error("Caught exception: (".$e->getMessage().") ");
456 if ( !$result['success'] ) {
457 sugar_rename($new_destination, str_replace($bean_id.'_'.$file_name, $bean_id, $new_destination));
458 $bean->doc_type = 'Sugar';
460 if ( ! is_array($_SESSION['user_error_message']) )
461 $_SESSION['user_error_message'] = array();
463 $error_message = isset($result['errorMessage']) ? $result['errorMessage'] : $GLOBALS['app_strings']['ERR_EXTERNAL_API_SAVE_FAIL'];
464 $_SESSION['user_error_message'][] = $error_message;
468 unlink($new_destination);
475 * returns the path with file name to save an uploaded file
476 * @param string bean_id ID of the parent bean
479 function get_upload_path($bean_id)
481 $file_name = $bean_id;
483 // cn: bug 8056 - mbcs filename in urlencoding > 212 chars in Windows fails
484 $end = (strlen($file_name) > 212) ? 212 : strlen($file_name);
485 $ret_file_name = substr($file_name, 0, $end);
487 return "upload://$ret_file_name";
492 * @param string bean_id ID of the parent bean
493 * @param string file_name File's name
495 static public function unlink_file($bean_id,$file_name = '')
497 if(file_exists("upload://$bean_id$file_name")) {
498 return unlink("upload://$bean_id$file_name");
503 * Get upload file location prefix
504 * @return string prefix
506 public function get_upload_dir()
512 * Return real FS path of the file
513 * @param string $path
515 public static function realpath($path)
517 if(substr($path, 0, 9) == "upload://") {
518 $path = UploadStream::path($path);
520 $ret = realpath($path);
521 return $ret?$ret:$path;
525 * Return path of uploaded file relative to uploads dir
526 * @param string $path
528 public static function relativeName($path)
530 if(substr($path, 0, 9) == "upload://") {
531 $path = substr($path, 9);
539 * Upload file stream handler
543 const STREAM_NAME = "upload";
544 protected static $upload_dir;
547 * Method checks Suhosin restrictions to use streams in php
550 * @return bool is allowed stream or not
552 public static function getSuhosinStatus()
554 // looks like suhosin patch doesn't block protocols, only suhosin extension (tested on FreeBSD)
555 // if suhosin is not installed it is okay for us
556 if (extension_loaded('suhosin') == false)
560 $configuration = ini_get_all('suhosin', false);
562 // suhosin simulation is okay for us
563 if ($configuration['suhosin.simulation'] == true)
568 // checking that UploadStream::STREAM_NAME is allowed by white list
569 $streams = $configuration['suhosin.executor.include.whitelist'];
572 $streams = explode(',', $streams);
573 foreach($streams as $stream)
575 $stream = explode('://', $stream, 2);
576 if (count($stream) == 1)
578 if ($stream[0] == UploadStream::STREAM_NAME)
583 elseif ($stream[1] == '' && $stream[0] == UploadStream::STREAM_NAME)
589 $GLOBALS['log']->fatal('Stream ' . UploadStream::STREAM_NAME . ' is not listed in suhosin.executor.include.whitelist and blocked because of it');
593 // checking that UploadStream::STREAM_NAME is not blocked by black list
594 $streams = $configuration['suhosin.executor.include.blacklist'];
597 $streams = explode(',', $streams);
598 foreach($streams as $stream)
600 $stream = explode('://', $stream, 2);
601 if ($stream[0] == UploadStream::STREAM_NAME)
603 $GLOBALS['log']->fatal('Stream ' . UploadStream::STREAM_NAME . 'is listed in suhosin.executor.include.blacklist and blocked because of it');
610 $GLOBALS['log']->fatal('Suhosin blocks all streams, please define ' . UploadStream::STREAM_NAME . ' stream in suhosin.executor.include.whitelist');
615 * Get upload directory
618 public static function getDir()
620 if(empty(self::$upload_dir)) {
621 self::$upload_dir = rtrim($GLOBALS['sugar_config']['upload_dir'], '/\\');
622 if(empty(self::$upload_dir)) {
623 self::$upload_dir = "upload";
625 if(!file_exists(self::$upload_dir)) {
626 sugar_mkdir(self::$upload_dir, 0755, true);
629 return self::$upload_dir;
633 * Check if upload dir is writable
636 public static function writable()
638 return is_writable(self::getDir());
642 * Register the stream
644 public function register()
646 stream_register_wrapper(self::STREAM_NAME, __CLASS__);
650 * Get real FS path of the upload stream file
651 * @param string $path Upload stream path (with upload://)
652 * @return string FS path
654 public static function path($path)
656 $path = substr($path, strlen(self::STREAM_NAME)+3); // cut off upload://
657 $path = str_replace("\\", "/", $path); // canonicalize path
658 if($path == ".." || substr($path, 0, 3) == "../" || substr($path, -3, 3) == "/.." || strstr($path, "/../")) {
659 $GLOBALS['log']->fatal("Invalid uploaded file name supplied: $path");
662 return self::getDir()."/".$path;
666 * Ensure upload subdir exists
667 * @param string $path Upload stream path (with upload://)
668 * @param bool $writable
671 public static function ensureDir($path, $writable = true)
673 $path = self::path($path);
675 return sugar_mkdir($path, 0755, true);
680 public function dir_closedir()
682 closedir($this->dirp);
685 public function dir_opendir ($path, $options )
687 $this->dirp = opendir(self::path($path));
688 return !empty($this->dirp);
691 public function dir_readdir()
693 return readdir($this->dirp);
696 public function dir_rewinddir()
698 return rewinddir($this->dirp);
701 public function mkdir($path, $mode, $options)
703 return mkdir(self::path($path), $mode, ($options&STREAM_MKDIR_RECURSIVE) != 0);
706 public function rename($path_from, $path_to)
708 return rename(self::path($path_from), self::path($path_to));
711 public function rmdir($path, $options)
713 return rmdir(self::path($path));
716 public function stream_cast ($cast_as)
721 public function stream_close ()
727 public function stream_eof ()
729 return feof($this->fp);
731 public function stream_flush ()
733 return fflush($this->fp);
736 public function stream_lock($operation)
738 return flock($this->fp, $operation);
741 public function stream_open($path, $mode)
743 $fullpath = self::path($path);
744 if(empty($fullpath)) return false;
746 $this->fp = fopen($fullpath, $mode);
748 // if we will be writing, try to transparently create the directory
749 $this->fp = @fopen($fullpath, $mode);
750 if(!$this->fp && !file_exists(dirname($fullpath))) {
751 mkdir(dirname($fullpath), 0755, true);
752 $this->fp = fopen($fullpath, $mode);
755 return !empty($this->fp);
758 public function stream_read($count)
760 return fread($this->fp, $count);
763 public function stream_seek($offset, $whence = SEEK_SET)
765 return fseek($this->fp, $offset, $whence) == 0;
768 public function stream_set_option($option, $arg1, $arg2)
773 public function stream_stat()
775 return fstat($this->fp);
778 public function stream_tell()
780 return ftell($this->fp);
782 public function stream_write($data)
784 return fwrite($this->fp, $data);
787 public function unlink($path)
789 unlink(self::path($path));
793 public function url_stat($path, $flags)
795 return @stat(self::path($path));
798 public static function move_uploaded_file($upload, $path)
800 return move_uploaded_file($upload, self::path($path));