<?php /** * @package RSForm! Pro * @copyright (C) 2007-2019 www.rsjoomla.com * @license GPL, http://www.gnu.org/copyleft/gpl.html */ defined('_JEXEC') or die; use Joomla\CMS\Language\Text; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Factory; require_once JPATH_ADMINISTRATOR.'/components/com_rsform/helpers/field.php'; class RSFormProFieldFileUpload extends RSFormProField { // backend preview public function getPreviewInput() { return '<input type="file" />'; } // @desc Returns the full name of the name HTML tag (eg. form[textbox]) public function getName() { $name = $this->namespace.'['.$this->name.']'; if ($this->getProperty('MULTIPLE', false)) { $name .= '[]'; } return $name; } // functions used for rendering in front view public function getFormInput() { $multiple = $this->getProperty('MULTIPLE', false); $multipleplus = $this->getProperty('MULTIPLEPLUS', false); if ($multiple && $multipleplus) { $minFiles = (int) $this->getProperty('MINFILES', 1); // If we require a minimum number of files to be uploaded, let's show a separate input for each upload in order to help the user if ($minFiles > 1) { $html = str_repeat('<div class="rsfp-field-multiple-plus">' . $this->getFileInput() . '</div>', $minFiles); } else { $html = '<div class="rsfp-field-multiple-plus">' . $this->getFileInput() . '</div>'; } return $html . $this->getButtonInput(); } else { return $this->getFileInput(); } } public function getButtonInput() { $button = '<button type="button"'; if ($attr = $this->getButtonAttributes()) { foreach ($attr as $key => $value) { $button .= $this->attributeToHtml($key, $value); } } $maxFiles = (int) $this->getProperty('MAXFILES', 0); if ($maxFiles > 0) { $button .= ' data-rsfp-maxfiles="' . $maxFiles . '"'; } $minFiles = (int) $this->getProperty('MINFILES', 0); /* * We have only one maximum file => disable * We have a defined number of maximum files but we've set minimum files lower => disable */ if ($maxFiles === 1 || ($maxFiles > 0 && $minFiles >= $maxFiles)) { $button .= ' disabled'; } $button .= ' data-rsfp-formid="' . $this->formId . '"'; $button .= ' onclick="RSFormPro.addMoreFiles(this);">' . Text::_('COM_RSFORM_FILE_ADD_PLUS') . '</button>'; return $button; } public function getFileInput() { $name = $this->getName(); $id = $this->getId(); $attr = $this->getAttributes(); $multiple = $this->getProperty('MULTIPLE', false); $multipleplus = $this->getProperty('MULTIPLEPLUS', false); $type = 'file'; $additional = ''; // Start building the HTML input $html = '<input'; // Parse Additional Attributes if ($attr) { foreach ($attr as $key => $values) { // @new feature - Some HTML attributes (type) can be overwritten // directly from the Additional Attributes area if ($key == 'type' && strlen($values)) { ${$key} = $values; continue; } $additional .= $this->attributeToHtml($key, $values); } } // Set the type $html .= ' type="'.$this->escape($type).'"'; // Name & id $html .= ' name="'.$this->escape($name).'"'. ' id="'.$this->escape($id).'"'; if ($multiple) { if ($multipleplus) { $html .= ' data-rsfp-skip-ajax="true"'; } else { $html .= ' multiple'; } } if ($this->getProperty('ACCEPTEDFILESIMAGES') && $this->getProperty('SHOWIMAGEPREVIEW')) { $html .= ' onchange="RSFormPro.loadImage(this);"'; } // Additional HTML $html .= $additional; $html .= $this->addDataAttributes(); // Close the tag $html .= ' />'; return $html; } protected function getButtonAttributes() { return array('class' => 'rsfp-field-multiple-plus-button'); } protected function addDataAttributes() { $html = ''; if ($this->isRequired()) { $html .= ' data-rsfp-required="true"'; } if ($this->getProperty('MULTIPLE')) { $minFiles = (int) $this->getProperty('MINFILES', 1); $maxFiles = (int) $this->getProperty('MAXFILES', 0); if ($minFiles > 0) { $html .= ' data-rsfp-minfiles="' . $minFiles . '"'; } if ($maxFiles > 0) { $html .= ' data-rsfp-maxfiles="' . $maxFiles . '"'; } } if ($this->getProperty('ACCEPTEDFILESIMAGES')) { $acceptedExts = array('jpg', 'jpeg', 'png', 'gif'); $newWidth = (int) $this->getProperty('THUMBSIZE', 220); if ($newWidth === 0 || ($newWidth > 0 && version_compare(PHP_VERSION, '7.3.0', '>='))) { $acceptedExts[] = 'webp'; } } elseif ($exts = $this->getProperty('ACCEPTEDFILES')) { $acceptedExts = RSFormProHelper::explode($exts); } else { $acceptedExts = array(); } if ($acceptedExts) { array_walk($acceptedExts, array($this, 'cleanExtension')); $html .= ' data-rsfp-exts="' . $this->escape(json_encode($acceptedExts)) . '"'; } $size = (int) $this->getProperty('FILESIZE'); if ($size) { $html .= ' data-rsfp-size="' . ($size * 1024) . '"'; } $this->addScriptDeclaration('RSFormPro.Translations.add(' . $this->formId . ', ' . json_encode($this->name) . ', ' . json_encode('VALIDATIONMESSAGE') . ', ' . json_encode($this->getProperty('VALIDATIONMESSAGE')) . ');'); $this->addCommonTranslations(); return $html; } private function addCommonTranslations() { static $done; if (!$done) { $done = true; $messages = array( 'COM_RSFORM_FILE_EXCEEDS_LIMIT' => Text::_('COM_RSFORM_FILE_EXCEEDS_LIMIT'), 'COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED' => Text::_('COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED'), 'COM_RSFORM_MINFILES_REQUIRED' => Text::_('COM_RSFORM_MINFILES_REQUIRED'), 'COM_RSFORM_MAXFILES_REQUIRED' => Text::_('COM_RSFORM_MAXFILES_REQUIRED'), ); $script = ''; foreach ($messages as $key => $message) { $script .= 'RSFormPro.Translations.addCommonTranslation(' . json_encode($key) . ', ' . json_encode($message) . ');'; } $this->addScriptDeclaration($script); } } public function cleanExtension(&$value, $key = null) { $value = strtolower(trim($value)); } // @desc All upload fields should have a 'rsform-upload-box' class for easy styling public function getAttributes() { $attr = parent::getAttributes(); if (strlen($attr['class'])) { $attr['class'] .= ' '; } $attr['class'] .= 'rsform-upload-box'; return $attr; } // process the upload file after form validation public function processBeforeStore($submissionId, &$post, &$files, $addToDb = true) { if (!isset($files[$this->name])) { return false; } $allFiles = array(); $actualFiles = $this->getProperty('MULTIPLE', false) ? $files[$this->name] : array($files[$this->name]); foreach ($actualFiles as $actualFile) { if ($actualFile['error'] != UPLOAD_ERR_OK) { continue; } $prefixProperty = $this->getProperty('PREFIX', ''); $destination = RSFormProHelper::getRelativeUploadPath($this->getProperty('DESTINATION', '')); $sanitize = $this->getProperty('SANITIZEFILENAME', false); // Prefix $prefix = uniqid('') . '-'; if (strlen(trim($prefixProperty)) > 0) { $prefix = $this->isCode($prefixProperty); } // Path $realpath = realpath($destination . DIRECTORY_SEPARATOR); if (substr($realpath, -1) != DIRECTORY_SEPARATOR) { $realpath .= DIRECTORY_SEPARATOR; } // Filename if ($sanitize) { $file = $realpath . $prefix . $this->sanitize($actualFile['name']); } else { $file = $realpath . $prefix . $actualFile['name']; } // Upload File if (File::upload($actualFile['tmp_name'], $file, false, (bool) RSFormProHelper::getConfig('allow_unsafe'))) { if ($this->getProperty('ACCEPTEDFILESIMAGES', false)) { if (function_exists('imagecreatefromstring')) { $newWidth = (int) $this->getProperty('THUMBSIZE', 220); if ($newWidth > 0) { $image = @imagecreatefromstring(file_get_contents($file)); if ($image !== false) { $quality = (int) $this->getProperty('THUMBQUALITY', 75); $extension = $this->getProperty('THUMBEXTENSION', 'jpg'); // Try to rotate it, JPEG only $this->tryToRotate($image, $file); // If we're downsizing, IMG_BICUBIC produces better results if ($newWidth < imagesx($image)) { $image = imagescale($image, $newWidth, -1, IMG_BICUBIC); } else { $image = imagescale($image, $newWidth); } if ($image !== false) { $thumbFile = File::stripExt($file) . '.' . $extension; // Delete old file, we no longer need it File::delete($file); if ($extension === 'png') { imagealphablending($image, false); imagesavealpha($image, true); imagepng($image, $thumbFile); } elseif ($extension === 'jpg') { imagejpeg($image, $thumbFile, $quality); } elseif ($extension === 'webp' && function_exists('imagewebp')) { imagewebp($image, $thumbFile, $quality); } $file = $thumbFile; unset($image); } } } } else { Factory::getApplication()->enqueueMessage('COM_RSFORM_CREATING_THUMBNAILS_FROM_IMAGES_REQUIRES_GD', 'warning'); } } // Trigger Event - onBeforeStoreSubmissions Factory::getApplication()->triggerEvent('onRsformFrontendAfterFileUpload', array(array('formId' => $this->formId, 'submissionId' => $submissionId, 'fieldname' => $this->name, 'file' => $file, 'name' => $prefix . $actualFile['name'], 'addToDb' => $addToDb))); $allFiles[] = $file; } } if (!$allFiles) { return false; } $object = (object) array( 'SubmissionId' => $submissionId, 'FormId' => $this->formId, 'FieldName' => $this->name, 'FieldValue' => implode("\n", $allFiles) ); if ($addToDb) { Factory::getDbo()->insertObject('#__rsform_submission_values', $object, 'SubmissionValueId'); } return $object; } protected function tryToRotate(&$image, $file) { if (!function_exists('exif_read_data') || !function_exists('exif_imagetype') || !function_exists('imagerotate')) { return false; } if (exif_imagetype($file) !== IMAGETYPE_JPEG) { return false; } $data = exif_read_data($file); if ($data === false || !isset($data['Orientation']) || $data['Orientation'] == 1) { return false; } switch ($data['Orientation']) { case 2: $image = $this->imageFlip($image, 2); break; case 3: $image = $this->imageFlip($image, 3); break; case 4: $image = $this->imageFlip($image, 3); $image = $this->imageFlip($image, 2); break; case 5: $image = imagerotate($image, 270, 0); $image = $this->imageFlip($image, 2); break; case 6: $image = imagerotate($image, 270, 0); break; case 7: $image = $this->imageFlip($image, 2); $image = imagerotate($image, 270, 0); break; case 8: $image = imagerotate($image, 90, 0); break; } return true; } protected function imageFlip($imgsrc, $mode) { $width = imagesx($imgsrc); $height = imagesy($imgsrc); $src_x = 0; $src_y = 0; $src_width = $width; $src_height = $height; switch ($mode) { case 1: $src_y = $height - 1; $src_height = -$height; break; case 2: $src_x = $width - 1; $src_width = -$width; break; case 3: $src_x = $width - 1; $src_y = $height - 1; $src_width = -$width; $src_height = -$height; break; default: return $imgsrc; break; } $imgdest = imagecreatetruecolor($width, $height); if (imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y , $width, $height, $src_width, $src_height)) { return $imgdest; } return $imgsrc; } protected function sanitize($string) { // Remove any '-' from the string since they will be used as concatenaters $str = str_replace('-', ' ', $string); // Transliterate on the current language $str = Factory::getLanguage()->transliterate($str); // Trim white spaces at beginning and end $str = trim($str); // Remove any duplicate whitespace, and ensure all characters are alphanumeric $str = preg_replace('/(\s|[^A-Za-z0-9\-\.])+/', '-', $str); // Trim dashes at beginning and end of alias $str = trim($str, '-'); return $str; } public function md5(&$item, $key) { $item = md5($item); } public function removeHashedValues(&$form, $delete) { if (empty($form) || !is_array($form) || empty($delete) || !is_array($delete)) { return false; } $hashes = $form; array_walk($hashes, array($this, 'md5')); foreach ($delete as $hashToDelete) { $position = array_search($hashToDelete, $hashes); if ($position !== false) { if (is_file($form[$position])) { File::delete($form[$position]); } unset($form[$position]); } } return true; } public function processValidation($validationType = 'form', $submissionId = 0) { $db = Factory::getDbo(); $required = $this->isRequired(); $multiple = $this->getProperty('MULTIPLE', false); $files = Factory::getApplication()->input->files->get('form', null, 'raw'); if ($validationType == 'directory') { $query = $db->getQuery(true) ->select($db->qn('FieldValue')) ->from($db->qn('#__rsform_submission_values')) ->where($db->qn('FieldName') . ' = ' . $db->q($this->name)) ->where($db->qn('SubmissionId') . ' = ' . $db->q($submissionId)); if ($alreadyUploaded = $db->setQuery($query)->loadResult()) { $alreadyUploaded = RSFormProHelper::explode($alreadyUploaded); } else { $alreadyUploaded = array(); } $delete = Factory::getApplication()->input->post->get('delete', array(), 'array'); if (!empty($delete[$this->name])) { $this->removeHashedValues($alreadyUploaded, $delete[$this->name]); } } try { // No $_FILES, but required if (!$files && $required) { return false; } // $_FILES exists but not for our own field if (!isset($files[$this->name])) { $actualFiles = array(); } else { if ($multiple) { $actualFiles = $files[$this->name]; } else { $actualFiles = array($files[$this->name]); } } // Since we can't rely on counting $_FILES we need to count each correct file $countFiles = 0; $allowImages = $this->getProperty('ACCEPTEDFILESIMAGES', false); $newWidth = (int) $this->getProperty('THUMBSIZE', 220); foreach ($actualFiles as $actualFile) { $name = $actualFile['name']; $error = $actualFile['error']; $size = $actualFile['size']; // File has been uploaded correctly to the server if ($error == UPLOAD_ERR_OK) { // Let's check if the extension is allowed $extParts = explode('.', $name); $ext = strtolower(end($extParts)); $acceptedExts = false; if ($allowImages) { $acceptedExts = array('jpg', 'jpeg', 'png', 'gif'); if ($newWidth === 0 || ($newWidth > 0 && version_compare(PHP_VERSION, '7.3.0', '>='))) { $acceptedExts[] = 'webp'; } } elseif ($exts = $this->getProperty('ACCEPTEDFILES')) { $acceptedExts = RSFormProHelper::explode($exts); } // Let's check only if accepted extensions are set if ($acceptedExts) { $accepted = false; foreach ($acceptedExts as $acceptedExt) { $acceptedExt = trim(strtolower($acceptedExt)); if (strlen($acceptedExt) && $acceptedExt == $ext) { $accepted = true; break; } } if (!$accepted) { throw new Exception(Text::sprintf('COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8'))); } if ($allowImages && function_exists('exif_imagetype')) { $imagetype = exif_imagetype($actualFile['tmp_name']); if ($imagetype === false && ($ext === 'webp' && version_compare(PHP_VERSION, '7.1.0', '>=') || $ext !== 'webp')) { throw new Exception(Text::sprintf('COM_RSFORM_FILE_DOES_NOT_SEEM_TO_BE_AN_IMAGE', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8'))); } } } $filesize = (int) $this->getProperty('FILESIZE'); // Let's check if it's the correct size if ($size > 0 && $filesize > 0 && $size > $filesize * 1024) { throw new Exception(Text::sprintf('COM_RSFORM_FILE_EXCEEDS_LIMIT', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8'), $filesize)); } $countFiles++; } elseif ($error != UPLOAD_ERR_NO_FILE) { // Parse the error message switch ($error) { default: // File has not been uploaded correctly throw new Exception(Text::_('RSFP_FILE_HAS_NOT_BEEN_UPLOADED_DUE_TO_AN_UNKNOWN_ERROR')); break; case UPLOAD_ERR_INI_SIZE: throw new Exception(Text::_('RSFP_UPLOAD_ERR_INI_SIZE')); break; case UPLOAD_ERR_FORM_SIZE: throw new Exception(Text::_('RSFP_UPLOAD_ERR_FORM_SIZE')); break; case UPLOAD_ERR_PARTIAL: throw new Exception(Text::_('RSFP_UPLOAD_ERR_PARTIAL')); break; case UPLOAD_ERR_NO_TMP_DIR: throw new Exception(Text::_('RSFP_UPLOAD_ERR_NO_TMP_DIR')); break; case UPLOAD_ERR_CANT_WRITE: throw new Exception(Text::_('RSFP_UPLOAD_ERR_CANT_WRITE')); break; case UPLOAD_ERR_EXTENSION: throw new Exception(Text::_('RSFP_UPLOAD_ERR_EXTENSION')); break; } } } if ($multiple) { $minFiles = $this->getProperty('MINFILES', 1); $maxFiles = $this->getProperty('MAXFILES', 0); if ($validationType == 'directory') { $countFiles += count($alreadyUploaded); } if ($required || $countFiles) { if ($minFiles > 0 && $countFiles < $minFiles) { throw new Exception(Text::sprintf('COM_RSFORM_MINFILES_REQUIRED', $minFiles)); } if ($maxFiles > 0 && $countFiles > $maxFiles) { throw new Exception(Text::sprintf('COM_RSFORM_MAXFILES_REQUIRED', $maxFiles)); } } } if ($required && $countFiles === 0 && empty($alreadyUploaded)) { throw new Exception($this->getProperty('VALIDATIONMESSAGE')); } } catch (Exception $e) { $properties =& RSFormProHelper::getComponentProperties($this->componentId); $properties['VALIDATIONMESSAGE'] = $e->getMessage(); return false; } return true; } }