Index: branches/5.2.x/core/kernel/utility/formatters/upload_formatter.php =================================================================== --- branches/5.2.x/core/kernel/utility/formatters/upload_formatter.php (revision 16686) +++ branches/5.2.x/core/kernel/utility/formatters/upload_formatter.php (revision 16687) @@ -1,658 +1,657 @@ fileHelper = $this->Application->recallObject('FileHelper'); if ( $this->DestinationPath ) { $this->FullPath = FULL_PATH . $this->DestinationPath; } } /** * Sets field option defaults. * * @param string $field_name Field nae. * @param array $field_options Field options. * @param kDBBase $object Object. * * @return void */ public function PrepareOptions($field_name, &$field_options, &$object) { if ( !$this->DestinationPath && !isset($field_options['upload_dir']) ) { $base_path = $this->Application->getUnitOption($object->Prefix, 'BasePath'); $field_options['upload_dir'] = WRITEBALE_BASE . '/' . basename($base_path) . '/'; } if ( !isset($field_options['max_size']) ) { $field_options['max_size'] = MAX_UPLOAD_SIZE; } } /** * Processes file uploads from form * * @param mixed $value * @param string $field_name * @param kDBItem $object * @return mixed * @access public */ public function Parse($value, $field_name, &$object) { $value = $this->Application->HttpQuery->unescapeRequestVariable($value); $options = $object->GetFieldOptions($field_name); if ( getArrayValue($options, 'upload_dir') ) { $this->DestinationPath = $options['upload_dir']; $this->FullPath = FULL_PATH . $this->DestinationPath; } if ( is_array($value) && isset($value['tmp_ids']) ) { $ret = $this->_processFlashUploader($value, $field_name, $object); } else { $ret = $this->_processRegularUploader($value, $field_name, $object); } if ( getArrayValue($options, 'upload_dir') ) { $this->DestinationPath = null; $this->FullPath = null; } return $ret; } /** * Handles uploaded files, provided by Flash uploader * * @param Array|string $value * @param string $field_name * @param kDBItem $object * @return string * @access protected */ protected function _processFlashUploader($value, $field_name, $object) { $options = $object->GetFieldOptions($field_name); $this->sorting = isset($value['order']) ? explode('|', $value['order']) : Array (); if ( $value['tmp_deleted'] ) { $n_upload = Array (); $deleted = explode('|', $value['tmp_deleted']); $upload = explode('|', $value['upload']); foreach ($upload as $name) { if ( in_array($name, $deleted) ) { continue; } $n_upload[] = $name; } $value['upload'] = implode('|', $n_upload); } if ( !$value['tmp_ids'] ) { // no pending files -> return already uploaded files return $this->_sortFiles($value['upload']); } $swf_uploaded_ids = explode('|', $value['tmp_ids']); $swf_uploaded_names = explode('|', $value['tmp_names']); $existing = $value['upload'] ? explode('|', $value['upload']) : Array (); $fret = Array (); $max_files = $this->_getMaxFiles($options); $pending_actions = $object->getPendingActions(); $files_to_delete = $this->_getFilesToDelete($object); for ($i = 0; $i < min($max_files, count($swf_uploaded_ids)); $i++) { // don't delete uploaded file, when it's name matches delete file name $real_name = $this->_getRealFilename($swf_uploaded_names[$i], $options, $object, $files_to_delete); $file_name = $this->FullPath . $real_name; $tmp_file = WRITEABLE . '/tmp/' . $swf_uploaded_ids[$i] . '_' . $swf_uploaded_names[$i]; rename($tmp_file, $file_name); @chmod($file_name, 0666); $fret[] = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath . $real_name; $pending_actions[] = Array ( 'action' => 'make_live', 'id' => $object->GetID(), 'field' => $field_name, 'file' => $file_name ); $this->_renameFileInSorting($swf_uploaded_names[$i], $real_name); } $object->setPendingActions($pending_actions); return $this->_sortFiles(array_merge($existing, $fret)); } /** * Returns files, scheduled for deleting * * @param kDBItem $object * @return Array * @access protected */ protected function _getFilesToDelete($object) { $ret = Array (); foreach ($object->getPendingActions() as $data) { if ( $data['action'] == 'delete' ) { $ret[] = $data['file']; } } return $ret; } /** * Handles regular file upload * * @param string|Array $value * @param string $field_name * @param kDBItem $object * @return string * @access protected */ protected function _processRegularUploader($value, $field_name, $object) { $ret = !is_array($value) ? $value : ''; $options = $object->GetFieldOptions($field_name); if ( getArrayValue($value, 'upload') && getArrayValue($value, 'error') == UPLOAD_ERR_NO_FILE ) { // file was not uploaded this time, but was uploaded before, then use previously uploaded file (from db) return getArrayValue($value, 'upload'); } if ( is_array($value) && count($value) > 1 && $value['size'] ) { if ( is_array($value) && (int)$value['error'] === UPLOAD_ERR_OK ) { // we can get mime type based on file content and don't use one, provided by the client // $value['type'] = kUtil::mimeContentType($value['tmp_name']); - if ( getArrayValue($options, 'file_types') && !$this->extensionMatch($value['name'], $options['file_types']) ) { + if ( getArrayValue($options, 'file_types') + && !$this->fileHelper->extensionMatch($value['name'], $options['file_types']) + ) { // match by file extensions $error_params = Array ( 'file_name' => $value['name'], 'file_types' => $options['file_types'], ); $object->SetError($field_name, 'bad_file_format', 'la_error_InvalidFileFormat', $error_params); } elseif ( getArrayValue($options, 'allowed_types') && !in_array($value['type'], $options['allowed_types']) ) { // match by mime type provided by web-browser $error_params = Array ( 'file_type' => $value['type'], 'allowed_types' => $options['allowed_types'], ); $object->SetError($field_name, 'bad_file_format', 'la_error_InvalidFileFormat', $error_params); } elseif ( $value['size'] > $options['max_size'] ) { $object->SetError($field_name, 'bad_file_size', 'la_error_FileTooLarge'); } elseif ( !is_writable($this->FullPath) ) { $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file'); } else { $tmp_path = WRITEABLE . '/tmp/'; $filename = $this->fileHelper->ensureUniqueFilename($tmp_path, $value['name'] . '.tmp'); $tmp_file_path = $tmp_path . $filename; $moved = move_uploaded_file($value['tmp_name'], $tmp_file_path); $storage_format = isset($options['storage_format']) ? $options['storage_format'] : false; if ( $storage_format ) { /** @var kUploadHelper $upload_helper */ $upload_helper = $this->Application->recallObject('kUploadHelper'); $moved = $upload_helper->resizeUploadedFile($tmp_file_path, $storage_format); } if ( $moved ) { $real_name = $this->_getRealFilename( kUtil::removeTempExtension(basename($tmp_file_path)), $options, $object ); $file_name = $this->FullPath . $real_name; $moved = rename($tmp_file_path, $file_name); } if ( !$moved ) { $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file'); } else { @chmod($file_name, 0666); if ( getArrayValue($options, 'size_field') ) { $object->SetDBField($options['size_field'], $value['size']); } if ( getArrayValue($options, 'orig_name_field') ) { $object->SetDBField($options['orig_name_field'], $value['name']); } if ( getArrayValue($options, 'content_type_field') ) { $object->SetDBField($options['content_type_field'], $value['type']); } $ret = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath . $real_name; // delete previous file, when new file is uploaded under same field /*$previous_file = isset($value['upload']) ? $value['upload'] : false; if ( $previous_file && file_exists($this->FullPath . $previous_file) ) { unlink($this->FullPath . $previous_file); }*/ } } } else { $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file'); } } if ( (count($value) > 1) && $value['error'] && ($value['error'] != UPLOAD_ERR_NO_FILE) ) { $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file', $value); } return $ret; } /** * Checks, that given file name has on of provided file extensions * - * @param string $filename - * @param string $file_types - * @return bool - * @access protected + * @param string $filename Filename. + * @param string $file_types File types. + * + * @return boolean + * @deprecated 5.2.2-B2 + * @see FileHelper::extensionMatch() */ protected function extensionMatch($filename, $file_types) { - if ( preg_match_all('/\*\.(.*?)(;|$)/', $file_types, $regs) ) { - $file_extension = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - $file_extensions = array_map('mb_strtolower', $regs[1]); - - return in_array($file_extension, $file_extensions); - } + kUtil::deprecatedMethod(__METHOD__, '5.2.2-B2', 'FileHelper::extensionMatch'); - return true; + return $this->fileHelper->extensionMatch($filename, $file_types); } /** * Resorts uploaded files according to given file order * * @param Array|string $files * @return string * @access protected */ protected function _sortFiles($files) { if ( !is_array($files) ) { $files = explode('|', $files); } $sorted_files = array_intersect($this->sorting, $files); // removes deleted files from sorting $new_files = array_diff($files, $sorted_files); // files, that weren't sorted - add to the end return implode('|', array_merge($sorted_files, $new_files)); } /** * Returns maximal allowed file count per field * * @param Array $options * @return int * @access protected */ protected function _getMaxFiles($options) { if ( !isset($options['multiple']) ) { return 1; } return $options['multiple'] == false ? 1 : $options['multiple']; } /** * Returns final filename after applying storage-engine specific naming * * @param string $file_name * @param Array $options * @param kDBItem $object * @param Array $files_to_delete * @return string * @access protected */ protected function _getRealFilename($file_name, $options, $object, $files_to_delete = Array ()) { $real_name = $this->getStorageEngineFile($file_name, $options, $object->Prefix); $real_name = $this->getStorageEngineFolder($real_name, $options) . $real_name; return $this->fileHelper->ensureUniqueFilename($this->FullPath, $real_name, $files_to_delete); } /** * Renames file in sorting list * * @param string $old_name * @param string $new_name * @return void * @access protected */ protected function _renameFileInSorting($old_name, $new_name) { $index = array_search($old_name, $this->sorting); if ( $index !== false ) { $this->sorting[$index] = $new_name; } } function getSingleFormat($format) { $single_mapping = Array ( 'file_raw_urls' => 'raw_url', 'file_display_names' => 'display_name', 'file_urls' => 'full_url', 'file_paths' => 'full_path', 'file_sizes' => 'file_size', 'files_resized' => 'resize', 'img_sizes' => 'img_size', 'wms' => 'wm', ); return $single_mapping[$format]; } /** * Return formatted file url,path or size (or same for multiple files) * * @param string $value * @param string $field_name * @param kDBItem|kDBList $object * @param string $format * @return string */ function Format($value, $field_name, &$object, $format = NULL) { if ( is_null($value) ) { return ''; } $options = $object->GetFieldOptions($field_name); if ( !isset($format) ) { $format = isset($options['format']) ? $options['format'] : false; } if ( $format && preg_match('/(file_raw_urls|file_display_names|file_urls|file_paths|file_names|file_sizes|img_sizes|files_resized|wms)(.*)/', $format, $regs) ) { if ( !$value || $format == 'file_names' ) { // storage format matches display format OR no value return $value; } $ret = Array (); $files = explode('|', $value); $format = $this->getSingleFormat($regs[1]) . $regs[2]; foreach ($files as $a_file) { $ret[] = $this->GetFormatted($a_file, $field_name, $object, $format); } return implode('|', $ret); } $tc_value = $this->TypeCast($value, $options); if ( ($tc_value === false) || ($tc_value != $value) ) { // for leaving badly formatted date on the form return $value; } return $this->GetFormatted($tc_value, $field_name, $object, $format); } /** * Return formatted file url,path or size * * @param string $value * @param string $field_name * @param kDBItem $object * @param string $format * @return string */ function GetFormatted($value, $field_name, &$object, $format = NULL) { if ( !$format ) { return $value; } $options = $object->GetFieldOptions($field_name); $upload_dir = isset($options['include_path']) && $options['include_path'] ? '' : $this->getUploadDir($options); $file_path = strlen($value) ? FULL_PATH . str_replace('/', DIRECTORY_SEPARATOR, $upload_dir) . $value : ''; if ( preg_match('/resize:([\d]*)x([\d]*)/', $format, $regs) ) { /** @var ImageHelper $image_helper */ $image_helper = $this->Application->recallObject('ImageHelper'); try { return $image_helper->ResizeImage($file_path, $format); } catch ( RuntimeException $e ) { // error, during image resize -> return empty string return ''; } } elseif ( !strlen($file_path) || !file_exists($file_path) ) { // file doesn't exist OR not uploaded return ''; } switch ($format) { case 'display_name': return kUtil::removeTempExtension($value); break; case 'raw_url': return $this->fileHelper->pathToUrl($file_path); break; case 'full_url': $direct_links = isset($options['direct_links']) ? $options['direct_links'] : true; if ( $direct_links ) { return $this->fileHelper->pathToUrl($file_path); } else { $url_params = Array ( 'pass' => 'm,'.$object->Prefix, $object->Prefix . '_event' => 'OnViewFile', 'file' => $value, 'field' => $field_name ); return $this->Application->HREF('', '', $url_params); } break; case 'full_path': return $file_path; break; case 'file_size': return filesize($file_path); break; case 'img_size': /** @var ImageHelper $image_helper */ $image_helper = $this->Application->recallObject('ImageHelper'); $image_info = $image_helper->getImageInfo($file_path); return $image_info ? $image_info[3] : ''; break; } return sprintf($format, $value); } /** * Creates & returns folder, based on storage engine specified in field options * * @param string $file_name * @param array $options * @return string * @access protected * @throws Exception */ protected function getStorageEngineFolder($file_name, $options) { $storage_engine = (string)getArrayValue($options, 'storage_engine'); if ( !$storage_engine ) { return ''; } switch ($storage_engine) { case StorageEngine::HASH: $folder_path = kUtil::getHashPathForLevel($file_name); break; case StorageEngine::TIMESTAMP: $folder_path = adodb_date('Y-m/d/'); break; default: throw new Exception('Unknown storage engine "' . $storage_engine . '".'); break; } return $folder_path; } /** * Applies prefix & suffix to uploaded filename, based on storage engine in field options * * @param string $name * @param array $options * @param string $unit_prefix * @return string * @access protected */ protected function getStorageEngineFile($name, $options, $unit_prefix) { $prefix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_prefix'), $unit_prefix); $suffix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_suffix'), $unit_prefix); $parts = pathinfo($name); return ($prefix ? $prefix . '_' : '') . $parts['filename'] . ($suffix ? '_' . $suffix : '') . '.' . $parts['extension']; } /** * Creates prefix/suffix to join with uploaded file * * Added "u" before user_id to keep this value after FileHelper::ensureUniqueFilename method call * * @param string $option * @param string $unit_prefix * @return string * @access protected */ protected function getStorageEngineFilePart($option, $unit_prefix) { $replace_from = Array ( StorageEngine::PS_DATE_TIME, StorageEngine::PS_PREFIX, StorageEngine::PS_USER ); $replace_to = Array ( adodb_date('Ymd-His'), $unit_prefix, 'u' . $this->Application->RecallVar('user_id') ); return str_replace($replace_from, $replace_to, $option); } public function getUploadDir($options) { return isset($options['upload_dir']) ? $options['upload_dir'] : $this->DestinationPath; } } class kPictureFormatter extends kUploadFormatter { public function __construct() { $this->NakeLookupPath = IMAGES_PATH; // used ? $this->DestinationPath = kUtil::constOn('ADMIN') ? IMAGES_PENDING_PATH : IMAGES_PATH; parent::__construct(); } /** * Return formatted file url,path or size * * @param string $value * @param string $field_name * @param kDBItem $object * @param string $format * @return string */ function GetFormatted($value, $field_name, &$object, $format = NULL) { if ( $format == 'img_size' ) { $options = $object->GetFieldOptions($field_name); $img_path = FULL_PATH . '/' . $this->getUploadDir($options) . $value; $image_info = getimagesize($img_path); return ' ' . $image_info[3]; } return parent::GetFormatted($value, $field_name, $object, $format); } } Index: branches/5.2.x/core/units/helpers/file_helper.php =================================================================== --- branches/5.2.x/core/units/helpers/file_helper.php (revision 16686) +++ branches/5.2.x/core/units/helpers/file_helper.php (revision 16687) @@ -1,484 +1,505 @@ Application->ConfigValue($object->Prefix.'_MaxImageCount'); // file count equals to image count (temporary measure) $sql = 'SELECT * FROM '.TABLE_PREFIX.'CatalogFiles WHERE ResourceId = '.$object->GetDBField('ResourceId').' ORDER BY FileId ASC LIMIT 0, '.(int)$max_file_count; $item_files = $this->Conn->Query($sql); $file_counter = 1; foreach ($item_files as $item_file) { $file_path = $item_file['FilePath']; $object->SetDBField('File'.$file_counter, $file_path); $object->SetOriginalField('File'.$file_counter, $file_path); $object->SetFieldOption('File'.$file_counter, 'original_field', $item_file['FileName']); $file_counter++; } } /** * Saves newly uploaded images to external image table * * @param kCatDBItem $object * @return void * @access public */ public function SaveItemFiles(&$object) { $table_name = $this->Application->getUnitOption('#file', 'TableName'); $max_file_count = $this->Application->getUnitOption($object->Prefix, 'FileCount'); // $this->Application->ConfigValue($object->Prefix.'_MaxImageCount'); $this->CheckFolder(FULL_PATH . ITEM_FILES_PATH); $i = 0; while ($i < $max_file_count) { $field = 'File'.($i + 1); $field_options = $object->GetFieldOptions($field); $file_path = $object->GetDBField($field); if ($file_path) { if (isset($field_options['original_field'])) { $key_clause = 'FileName = '.$this->Conn->qstr($field_options['original_field']).' AND ResourceId = '.$object->GetDBField('ResourceId'); if ($object->GetDBField('Delete'.$field)) { // if item was cloned, then new filename is in db (not in $image_src) $sql = 'SELECT FilePath FROM '.$table_name.' WHERE '.$key_clause; $file_path = $this->Conn->GetOne($sql); if (@unlink(FULL_PATH.ITEM_FILES_PATH.$file_path)) { $sql = 'DELETE FROM '.$table_name.' WHERE '.$key_clause; $this->Conn->Query($sql); } } else { // image record found -> update $fields_hash = Array ( 'FilePath' => $file_path, ); $this->Conn->doUpdate($fields_hash, $table_name, $key_clause); } } else { // record not found -> create $fields_hash = Array ( 'ResourceId' => $object->GetDBField('ResourceId'), 'FileName' => $field, 'Status' => STATUS_ACTIVE, 'FilePath' => $file_path, ); $this->Conn->doInsert($fields_hash, $table_name); $field_options['original_field'] = $field; $object->SetFieldOptions($field, $field_options); } } $i++; } } /** * Preserves cloned item images/files to be rewritten with original item images/files * * @param Array $field_values * @return void * @access public */ public function PreserveItemFiles(&$field_values) { foreach ($field_values as $field_name => $field_value) { if ( !is_array($field_value) ) { continue; } if ( isset($field_value['upload']) && ($field_value['error'] == UPLOAD_ERR_NO_FILE) ) { // this is upload field, but nothing was uploaded this time unset($field_values[$field_name]); } } } /** * Determines what image/file fields should be created (from post or just dummy fields for 1st upload) * * @param string $prefix * @param bool $is_image * @return void * @access public */ public function createItemFiles($prefix, $is_image = false) { $items_info = $this->Application->GetVar($prefix); if ($items_info) { list (, $fields_values) = each($items_info); $this->createUploadFields($prefix, $fields_values, $is_image); } else { $this->createUploadFields($prefix, Array(), $is_image); } } /** * Dynamically creates virtual fields for item for each image/file field in submit * * @param string $prefix * @param Array $fields_values * @param bool $is_image * @return void * @access public */ public function createUploadFields($prefix, $fields_values, $is_image = false) { $field_options = Array ( 'type' => 'string', 'max_len' => 240, 'default' => '', ); if ($is_image) { $field_options['formatter'] = 'kPictureFormatter'; $field_options['include_path'] = 1; $field_options['allowed_types'] = Array ('image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png', 'image/gif', 'image/bmp'); $field_prefix = 'Image'; } else { $field_options['formatter'] = 'kUploadFormatter'; $field_options['upload_dir'] = ITEM_FILES_PATH; $field_options['allowed_types'] = Array ('application/pdf', 'application/msexcel', 'application/msword', 'application/mspowerpoint'); $field_prefix = 'File'; } $fields = $this->Application->getUnitOption($prefix, 'Fields'); $virtual_fields = $this->Application->getUnitOption($prefix, 'VirtualFields'); $image_count = 0; foreach ($fields_values as $field_name => $field_value) { if (preg_match('/^('.$field_prefix.'[\d]+|Primary'.$field_prefix.')$/', $field_name)) { $fields[$field_name] = $field_options; $virtual_fields[$field_name] = $field_options; $this->_createCustomFields($prefix, $field_name, $virtual_fields, $is_image); $image_count++; } } if (!$image_count) { // no images found in POST -> create default image fields $image_count = $this->Application->ConfigValue($prefix.'_MaxImageCount'); if ($is_image) { $created_count = 1; $image_names = Array ('Primary' . $field_prefix => ''); while ($created_count < $image_count) { $image_names[$field_prefix . $created_count] = ''; $created_count++; } } else { $created_count = 0; $image_names = Array (); while ($created_count < $image_count) { $image_names[$field_prefix . ($created_count + 1)] = ''; $created_count++; } } if ($created_count) { $this->createUploadFields($prefix, $image_names, $is_image); } return ; } $this->Application->setUnitOption($prefix, $field_prefix.'Count', $image_count); $this->Application->setUnitOption($prefix, 'Fields', $fields); $this->Application->setUnitOption($prefix, 'VirtualFields', $virtual_fields); } /** * Adds ability to create more virtual fields associated with main image/file * * @param string $prefix * @param string $field_name * @param Array $virtual_fields * @param bool $is_image * @return void * @access protected */ protected function _createCustomFields($prefix, $field_name, &$virtual_fields, $is_image = false) { $virtual_fields['Delete' . $field_name] = Array ('type' => 'int', 'default' => 0); if ( $is_image ) { $virtual_fields[$field_name . 'Alt'] = Array ('type' => 'string', 'default' => ''); } } /** * Downloads file to user * * @param string $filename * @return void * @access public */ public function DownloadFile($filename) { $this->Application->setContentType(kUtil::mimeContentType($filename), false); header('Content-Disposition: attachment; filename="' . basename($filename) . '"'); header('Content-Length: ' . filesize($filename)); readfile($filename); flush(); } /** * Creates folder with given $path * * @param string $path * @return bool * @access public */ public function CheckFolder($path) { $result = true; if (!file_exists($path) || !is_dir($path)) { $parent_path = preg_replace('#(/|\\\)[^/\\\]+(/|\\\)?$#', '', rtrim($path , '/\\')); $result = $this->CheckFolder($parent_path); if ($result) { $result = mkdir($path); if ($result) { chmod($path, 0777); // don't commit any files from created folder if (file_exists(FULL_PATH . '/CVS')) { $cvsignore = fopen($path . '/.cvsignore', 'w'); fwrite($cvsignore, '*.*'); fclose($cvsignore); chmod($path . '/.cvsignore', 0777); } } else { trigger_error('Cannot create directory "' . $path . '"', E_USER_WARNING); return false; } } } return $result; } /** * Copies all files and directories from $source to $destination directory. Create destination directory, when missing. * * @param string $source * @param string $destination * @return bool * @access public */ public function copyFolderRecursive($source, $destination) { if ( substr($source, -1) == DIRECTORY_SEPARATOR ) { $source = substr($source, 0, -1); $destination .= DIRECTORY_SEPARATOR . basename($source); } $iterator = new DirectoryIterator($source); /** @var DirectoryIterator $file_info */ $result = $this->CheckFolder($destination); foreach ($iterator as $file_info) { if ( $file_info->isDot() ) { continue; } $file = $file_info->getFilename(); if ( $file_info->isDir() ) { $result = $this->copyFolderRecursive($file_info->getPathname(), $destination . DIRECTORY_SEPARATOR . $file); } else { $result = copy($file_info->getPathname(), $destination . DIRECTORY_SEPARATOR . $file); } if (!$result) { trigger_error('Cannot create file/directory "' . $destination . DIRECTORY_SEPARATOR . $file . '"', E_USER_WARNING); break; } } return $result; } /** * Copies all files from $source to $destination directory. Create destination directory, when missing. * * @param string $source * @param string $destination * @return bool * @access public */ public function copyFolder($source, $destination) { if ( substr($source, -1) == DIRECTORY_SEPARATOR ) { $source = substr($source, 0, -1); $destination .= DIRECTORY_SEPARATOR . basename($source); } $iterator = new DirectoryIterator($source); /** @var DirectoryIterator $file_info */ $result = $this->CheckFolder($destination); foreach ($iterator as $file_info) { if ( $file_info->isDot() || !$file_info->isFile() ) { continue; } $file = $file_info->getFilename(); $result = copy($file_info->getPathname(), $destination . DIRECTORY_SEPARATOR . $file); if ( !$result ) { trigger_error('Cannot create file "' . $destination . DIRECTORY_SEPARATOR . $file . '"', E_USER_WARNING); break; } } return $result; } /** * Transforms given path to file into it's url, where each each component is encoded (excluding domain and protocol) * * @param string $url * @return string * @access public */ public function pathToUrl($url) { $url = str_replace(DIRECTORY_SEPARATOR, '/', preg_replace('/^' . preg_quote(FULL_PATH, '/') . '(.*)/', '\\1', $url, 1)); // TODO: why? $url = implode('/', array_map('rawurlencode', explode('/', $url))); return rtrim($this->Application->BaseURL(), '/') . $url; } /** * Transforms given url to path to it * * @param string $url * @return string * @access public */ public function urlToPath($url) { $base_url = rtrim($this->Application->BaseURL(), '/'); // escape replacement patterns, like "\" $full_path = preg_replace('/(\\\[\d]+)/', '\\\\\1', FULL_PATH); $path = preg_replace('/^' . preg_quote($base_url, '/') . '(.*)/', $full_path . '\\1', $url, 1); return str_replace('/', DIRECTORY_SEPARATOR, kUtil::unescape($path, kUtil::ESCAPE_URL)); } /** * Makes given paths DocumentRoot agnostic. * * @param array $paths List of file paths. * * @return array */ public function makeRelative(array $paths) { foreach ( $paths as $index => $path ) { $replaced_count = 0; $relative_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $path, 1, $replaced_count); if ( $replaced_count === 1 ) { $paths[$index] = $relative_path; } } return $paths; } /** * Ensures, that new file will not overwrite any of previously created files with same name * * @param string $path * @param string $name * @param Array $forbidden_names * @return string */ public function ensureUniqueFilename($path, $name, $forbidden_names = Array ()) { $parts = pathinfo($name); $ext = '.' . $parts['extension']; $filename = $parts['filename']; $path = rtrim($path, '/'); $original_checked = false; $new_name = $filename . $ext; if ( $parts['dirname'] != '.' ) { $path .= '/' . ltrim($parts['dirname'], '/'); } // make sure target folder always exists, especially for cases, // when storage engine folder is supplied as a part of $name $this->CheckFolder($path); while (file_exists($path . '/' . $new_name) || in_array($path . '/' . $new_name, $forbidden_names)) { if ( preg_match('/(.*)_([0-9]*)(' . preg_quote($ext, '/') . ')/', $new_name, $regs) ) { $new_name = $regs[1] . '_' . ((int)$regs[2] + 1) . $regs[3]; } elseif ( $original_checked ) { $new_name = $filename . '_1' . $ext; } $original_checked = true; } if ( $parts['dirname'] != '.' ) { $new_name = $parts['dirname'] . '/' . $new_name; } return $new_name; } + + /** + * Checks, that given file name has on of provided file extensions + * + * @param string $filename Filename. + * @param string $file_types File types. + * + * @return boolean + */ + public function extensionMatch($filename, $file_types) + { + if ( preg_match_all('/\*\.(.*?)(;|$)/', $file_types, $regs) ) { + $file_extension = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $file_extensions = array_map('mb_strtolower', $regs[1]); + + return in_array($file_extension, $file_extensions); + } + + return true; + } + } Index: branches/5.2.x/core/units/helpers/upload_helper.php =================================================================== --- branches/5.2.x/core/units/helpers/upload_helper.php (revision 16686) +++ branches/5.2.x/core/units/helpers/upload_helper.php (revision 16687) @@ -1,404 +1,418 @@ fileHelper = $this->Application->recallObject('FileHelper'); // 5 minutes execution time @set_time_limit(5 * 60); } /** * Handles the upload. * * @param kEvent $event Event. * * @return string * @throws kUploaderException When upload could not be handled properly. */ public function handle(kEvent $event) { $this->disableBrowserCache(); // Uncomment this one to fake upload time // sleep(5); if ( !$this->Application->HttpQuery->Post ) { // Variables {field, id, flashsid} are always submitted through POST! // When file size is larger, then "upload_max_filesize" (in php.ini), // then these variables also are not submitted. throw new kUploaderException('File size exceeds allowed limit.', 413); } if ( !$this->checkPermissions($event) ) { // 403 Forbidden throw new kUploaderException('You don\'t have permissions to upload.', 403); } $value = $this->Application->GetVar('file'); if ( !$value || ($value['error'] != UPLOAD_ERR_OK) ) { // 413 Request Entity Too Large (file uploads disabled OR uploaded file was // too large for web server to accept, see "upload_max_filesize" in php.ini) throw new kUploaderException('File size exceeds allowed limit.', 413); } $value = $this->Application->unescapeRequestVariable($value); $tmp_path = WRITEABLE . '/tmp/'; $filename = $this->getUploadedFilename() . '.tmp'; $id = $this->Application->GetVar('id'); if ( $id ) { $filename = $id . '_' . $filename; } if ( !is_writable($tmp_path) ) { // 500 Internal Server Error // check both temp and live upload directory throw new kUploaderException('Write permissions not set on the server, please contact server administrator.', 500); } $filename = $this->fileHelper->ensureUniqueFilename($tmp_path, $filename); - $storage_format = $this->getStorageFormat($this->Application->GetVar('field'), $event); + $field_options = $this->getFieldOptions($this->Application->GetVar('field'), $event); + $storage_format = isset($field_options['storage_format']) ? $field_options['storage_format'] : false; $file_path = $tmp_path . $filename; $actual_file_path = $this->moveUploadedFile($file_path); if ( $storage_format && $file_path == $actual_file_path ) { $this->resizeUploadedFile($file_path, $storage_format); } + if ( getArrayValue($field_options, 'file_types') + && !$this->fileHelper->extensionMatch(kUtil::removeTempExtension($filename), $field_options['file_types']) + ) { + throw new kUploaderException('File is not an allowed file type.', 415); + } + + if ( filesize($actual_file_path) > $field_options['max_size'] ) { + throw new kUploaderException('File size exceeds allowed limit.', 413); + } + $this->deleteTempFiles($tmp_path); $thumbs_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $tmp_path, 1); $thumbs_path = FULL_PATH . THUMBS_PATH . $thumbs_path; if ( file_exists($thumbs_path) ) { $this->deleteTempFiles($thumbs_path); } return preg_replace('/^' . preg_quote($id, '/') . '_/', '', basename($file_path)); } /** * Resizes uploaded file. * * @param string $file_path File path. * @param string $format Format. * * @return boolean */ public function resizeUploadedFile(&$file_path, $format) { /** @var ImageHelper $image_helper */ $image_helper = $this->Application->recallObject('ImageHelper'); // Add extension, so that "ImageHelper::ResizeImage" can work. $resize_file_path = tempnam(WRITEABLE . '/tmp', 'uploaded_') . '.jpg'; if ( rename($file_path, $resize_file_path) === false ) { return false; } $resized_file_path = $this->fileHelper->urlToPath( $image_helper->ResizeImage($resize_file_path, $format) ); $file_path = $this->replaceFileExtension( $file_path, pathinfo($resized_file_path, PATHINFO_EXTENSION) ); return rename($resized_file_path, $file_path); } /** * Replace extension of uploaded file. * * @param string $file_path File path. * @param string $new_file_extension New file extension. * * @return string */ protected function replaceFileExtension($file_path, $new_file_extension) { $file_path_without_temp_file_extension = kUtil::removeTempExtension($file_path); $current_file_extension = pathinfo($file_path_without_temp_file_extension, PATHINFO_EXTENSION); // Format of resized file wasn't changed. if ( $current_file_extension === $new_file_extension ) { return $file_path; } $ret = preg_replace( '/\.' . preg_quote($current_file_extension, '/') . '$/', '.' . $new_file_extension, $file_path_without_temp_file_extension ); // Add ".tmp" later, since it was removed. if ( $file_path_without_temp_file_extension !== $file_path ) { $ret .= '.tmp'; } // After file extension change resulting filename might not be unique in that folder anymore. $path = pathinfo($ret, PATHINFO_DIRNAME); return $path . '/' . $this->fileHelper->ensureUniqueFilename($path, basename($ret)); } /** * Sends headers to ensure, that response is never cached. * * @return void */ protected function disableBrowserCache() { header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); } /** * Checks, that flash uploader is allowed to perform upload * * @param kEvent $event * @return bool */ protected function checkPermissions(kEvent $event) { // Flash uploader does NOT send correct cookies, so we need to make our own check $cookie_name = 'adm_' . $this->Application->ConfigValue('SessionCookieName'); $this->Application->HttpQuery->Cookie['cookies_on'] = 1; $this->Application->HttpQuery->Cookie[$cookie_name] = $this->Application->GetVar('flashsid'); // this prevents session from auto-expiring when KeepSessionOnBrowserClose & FireFox is used $this->Application->HttpQuery->Cookie[$cookie_name . '_live'] = $this->Application->GetVar('flashsid'); /** @var Session $admin_session */ $admin_session = $this->Application->recallObject('Session.admin'); if ( $this->Application->permissionCheckingDisabled($admin_session->RecallVar('user_id')) ) { return true; } // copy some data from given session to current session $backup_user_id = $this->Application->RecallVar('user_id'); $this->Application->StoreVar('user_id', $admin_session->RecallVar('user_id')); $backup_user_groups = $this->Application->RecallVar('UserGroups'); $this->Application->StoreVar('UserGroups', $admin_session->RecallVar('UserGroups')); // check permissions using event, that have "add|edit" rule $check_event = new kEvent($event->getPrefixSpecial() . ':OnProcessSelected'); $check_event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true)); /** @var kEventHandler $event_handler */ $event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler'); $allowed_to_upload = $event_handler->CheckPermission($check_event); // restore changed data, so nothing gets saved to database $this->Application->StoreVar('user_id', $backup_user_id); $this->Application->StoreVar('UserGroups', $backup_user_groups); return $allowed_to_upload; } /** * Returns uploaded filename. * * @return string */ protected function getUploadedFilename() { if ( isset($_REQUEST['name']) ) { $file_name = $_REQUEST['name']; } elseif ( !empty($_FILES) ) { $file_name = $_FILES['file']['name']; } else { $file_name = uniqid('file_'); } return $file_name; } /** - * Gets storage format for a given field. + * Returns field options. * - * @param string $field_name - * @param kEvent $event - * @return bool + * @param string $field Field. + * @param kEvent $event Event. + * + * @return array */ - protected function getStorageFormat($field_name, kEvent $event) + protected function getFieldOptions($field, kEvent $event) { + /** @var array $fields */ $fields = $this->Application->getUnitOption($event->Prefix, 'Fields'); + + /** @var array $virtual_fields */ $virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields'); - $field_options = array_key_exists($field_name, $fields) ? $fields[$field_name] : $virtual_fields[$field_name]; - return isset($field_options['storage_format']) ? $field_options['storage_format'] : false; + return array_key_exists($field, $fields) ? $fields[$field] : $virtual_fields[$field]; } /** * Moves uploaded file to given location. * * @param string $file_path File path. * * @return string * @throws kUploaderException When upload could not be handled properly. */ protected function moveUploadedFile($file_path) { // Chunking might be enabled. $chunk = (int)$this->Application->GetVar('chunk', 0); $chunks = (int)$this->Application->GetVar('chunks', 0); $actual_file_path = $file_path . '.part'; // Open temp file. if ( !$out = @fopen($actual_file_path, $chunks ? 'ab' : 'wb') ) { throw new kUploaderException('Failed to open output stream.', 102); } if ( !empty($_FILES) ) { if ( $_FILES['file']['error'] || !is_uploaded_file($_FILES['file']['tmp_name']) ) { throw new kUploaderException('Failed to move uploaded file.', 103); } // Read binary input stream and append it to temp file. if ( !$in = @fopen($_FILES['file']['tmp_name'], 'rb') ) { throw new kUploaderException('Failed to open input stream.', 101); } } else { if ( !$in = @fopen('php://input', 'rb') ) { throw new kUploaderException('Failed to open input stream.', 101); } } while ( $buff = fread($in, 4096) ) { fwrite($out, $buff); } @fclose($out); @fclose($in); // Check if file has been uploaded. if ( !$chunks || $chunk == $chunks - 1 ) { // Strip the temp .part suffix off. rename($actual_file_path, $file_path); $actual_file_path = $file_path; } return $actual_file_path; } /** * Delete temporary files, that won't be used for sure * * @param string $path * @return void */ protected function deleteTempFiles($path) { $files = glob($path . '*.*'); $max_file_date = strtotime('-1 day'); foreach ( $files as $file ) { if ( filemtime($file) < $max_file_date ) { unlink($file); } } } /** * Prepares object for operations with file on given field. * * @param kEvent $event Event. * @param string $field Field. * * @return kDBItem */ public function prepareUploadedFile(kEvent $event, $field) { /** @var kDBItem $object */ $object = $event->getObject(Array ('skip_autoload' => true)); $filename = $this->getSafeFilename(); if ( !$filename ) { $object->SetDBField($field, ''); return $object; } // set current uploaded file if ( $this->Application->GetVar('tmp') ) { $options = $object->GetFieldOptions($field); $options['upload_dir'] = WRITEBALE_BASE . '/tmp/'; unset($options['include_path']); $object->SetFieldOptions($field, $options); $filename = $this->Application->GetVar('id') . '_' . $filename; } $object->SetDBField($field, $filename); return $object; } /** * Returns safe version of filename specified in url * * @return bool|string * @access protected */ protected function getSafeFilename() { $filename = $this->Application->GetVar('file'); $filename = $this->Application->unescapeRequestVariable($filename); if ( (strpos($filename, '../') !== false) || (trim($filename) !== $filename) ) { // when relative paths or special chars are found template names from url, then it's hacking attempt return false; } return $filename; } } class kUploaderException extends Exception { }