* @link http://www.tassos.gr * @copyright Copyright © 2020 Tassos Marinos All Rights Reserved * @license GNU GPLv3 or later */ use Joomla\Registry\Registry; use Joomla\String\StringHelper; use Joomla\Filter\InputFilter; use ConvertForms\Form; use ConvertForms\Helper; use ConvertForms\FieldsHelper; defined('_JEXEC') or die('Restricted access'); // import Joomla modelform library jimport('joomla.application.component.modeladmin'); /** * Conversion Model Class */ class ConvertFormsModelConversion extends JModelAdmin { /** * The database object * * @var object */ private $db; /** * Constructor. * * @param array $config An optional associative array of configuration settings. * * @see JModelLegacy * @since 1.6 */ public function __construct($config = array()) { parent::__construct($config); $this->db = JFactory::getDbo(); // Make sure our plugins are loaded. Without this line, the PDF plugin won't be able to catch the onContentAfterSave event. JPluginHelper::importPlugin('convertformstools'); } /** * Returns a reference to the a Table object, always creating it. * * @param type The table type to instantiate * @param string A prefix for the table class name. Optional. * @param array Configuration array for model. Optional. * @return JTable A database object * @since 2.5 */ public function getTable($type = 'Conversion', $prefix = 'ConvertFormsTable', $config = array()) { return JTable::getInstance($type, $prefix, $config); } /** * Allows preprocessing of the JForm object. * * @param JForm $form The form object * @param array $data The data to be merged into the form object * @param string $group The plugin group to be executed * * @return void * * @since 3.6.1 */ protected function preprocessForm(Joomla\CMS\Form\Form $form, $data, $group = 'content') { if (!isset($data->params)) { return parent::preprocessForm($form, $data, $group); } // Ensure the form binds the data regardless the letter case of the field's name. // We should not rely on the field's name. Instead we need to switch over to field's IDs instead. $data->params = array_change_key_case($data->params); // @todo - What we do here is somehow a joke. We should consider moving the logic of preparing each field to the respective field class in the namespace. $data_ = clone $data; $this->prepare($data_); // Add form custom fields to form $fields = []; foreach ($data_->prepared_fields as $key => $field) { $type = $field->options->get('type'); // Map of fields types that need to be transformed in order to be recognized by the XML parser. $transformFields = [ 'hidden' => 'text', 'currency' => 'NR_Currencies', 'country' => 'NR_Geo', 'checkbox' => 'checkboxes', 'dropdown' => 'list', 'fileupload' => 'textlist', 'confirm' => $field->options->get('confirm_type') ]; if ($type == 'fileupload') { $limit_files = $field->options->get('limit_files'); if (!isset($limit_files) || (isset($limit_files) && $limit_files == '1')) { $transformFields['fileupload'] = 'text'; // In case the previous multiple field is turn into a single field, we need to transform the value from array to string too. if (is_array($data->params[$key])) { $data->params[$key] = implode(',', $data->params[$key]); } } } if (array_key_exists($type, $transformFields)) { $type = $transformFields[$type]; } // Radio fields doesn't accept Array as a value and we need to transform it into a string. if (in_array($type, ['radio'])) { if (isset($data->params[$key])) { $data->params[$key] = implode('', (array) $data->params[$key]); } } // Create the field $label = $field->class->getLabel(); $fld = new SimpleXMLElement(''); $fld->addAttribute('name', $key); $fld->addAttribute('type', $type); $fld->addAttribute('label', $label); $fld->addAttribute('hint', $field->options->get('placeholder', $label)); $fld->addAttribute('description', $fld->attributes()->hint); $fld->addAttribute('class', 'input-xlarge'); $fld->addAttribute('rows', 10); // Used for textarea inputs $fld->addAttribute('filter', $field->options->get('filter', 'safehtml')); if ($type == 'editor') { $fld->addAttribute('editor', $field->options->get('editor')); } // Define options to list-based fields if (in_array($type, ['list', 'radio', 'checkboxes']) && $choices = $field->class->getOptions()) { if ($type == 'list' && $hint = $field->options->get('placeholder')) { array_unshift($choices, [ 'label' => trim($hint), 'value' => '', 'selected' => true ]); } foreach ($choices as $choice) { $option = $fld->addChild('option', htmlspecialchars($choice['label'])); $option->addAttribute('value', strip_tags($choice['value'])); } } // Get field's XML $fields[] = str_replace('', '', $fld->asXml()); } $form->setField(new SimpleXMLElement('
' . implode('', $fields) . '
')); parent::preprocessForm($form, $data, $group); } /** * Method to get the record form. * * @param array $data Data for the form. * @param boolean $loadData True if the form is to load its own data (default case), false if not. * @return mixed A JForm object on success, false on failure * @since 2.5 */ public function getForm($data = array(), $loadData = true) { // Get the form. $form = $this->loadForm('com_convertforms.conversion', 'conversion', array('control' => 'jform', 'load_data' => $loadData)); if (empty($form)) { return false; } return $form; } /** * Method to get the data that should be injected in the form. * * @return mixed The data for the form. */ protected function loadFormData() { // Check the session for previously entered form data. $data = JFactory::getApplication()->getUserState('com_convertforms.edit.conversion.data', array()); if (empty($data)) { $data = $this->getItem(null, false); } return $data; } /** * Validate data before saving * * @param object $form The form to validate * @param object $data The data to validate * @param string $group * * @return array The validated data */ public function validate($form, $data, $group = null) { // Validate conversion edited via the backend if (JFactory::getApplication()->isClient('administrator')) { return parent::validate($form, $data, $group); } // Make sure we have a valid Form data if (!isset($data['cf']) || empty($data['cf'])) { throw new Exception('No submission data found'); } // Make sure we have a valid Form ID passed if (!isset($data['cf']['form_id']) || !$formid = (int) $data['cf']['form_id']) { throw new Exception('Form ID is either missing or invalid'); } // Let the user manipulate the post data before saved into the database. // @todo - Move PHP Scripts logic to a separate plugin. $payload = ['post' => &$data['cf']]; Form::runPHPScript($formid, 'formprocess', $payload); // Get form from payload or load a new instance $form = isset($payload['form']) ? $payload['form'] : Form::load($formid); $error_message = null; $result = JFactory::getApplication()->triggerEvent('onConvertFormsSubmissionValidate', [&$data['cf'], &$error_message, $form]); if (in_array(false, $result, true)) { throw new \Exception(is_null($error_message) ? 'Error' : $error_message); } // Honeypot check if (isset($data['cf']['hnpt']) && !empty($data['cf']['hnpt'])) { throw new Exception('Honeypot field triggered'); die(); } // Make sure the right form is loaded if (is_null($form['id'])) { throw new Exception('Unknown Form'); } // Initialize the object that is going to be saved in the database $newData = [ 'form_id' => $formid, 'campaign_id' => (int) $form['params']['campaign'], 'state' => isset($form['params']['submission_state']) ? $form['params']['submission_state'] : 1 ]; $overrides = isset($data['overrides']) ? json_decode($data['overrides']) : []; // Let's validate submitted data foreach ($form['fields'] as $key => $form_field) { $field_name = isset($form_field['name']) ? $form_field['name'] : null; $field_class = FieldsHelper::getFieldClass($form_field['type'], $form_field, $data); $user_value = (!is_null($field_name) && isset($data['cf'][$field_name])) ? $data['cf'][$field_name] : null; // Check if the field must be ignored and not be validated. Eg: hidden by conditional logic. if (isset($overrides->ignore) && is_array($overrides->ignore) && in_array($form_field['key'], $overrides->ignore)) { continue; } // Validate and Filter user value. If an error occurs the submission aborts with an exception shown in the form $field_class->validate($user_value); // Skip unknown fields or fields with an empty value if (!$field_name || $user_value == '') { continue; } $newData['params'][$field_name] = $user_value; } return $newData; } /** * Create a new conversion based on the post data. * * @return object The new conversion row object */ public function createConversion($data) { JPluginHelper::importPlugin('convertforms'); JPluginHelper::importPlugin('convertformstools'); JPluginHelper::importPlugin('system'); // Validate data $data = $this->validate(null, $data); /** * This event is rather useful for the following reasons: * * 1. It allows us to make modifications to the submission after it passes the validation checks. * 2. It allows us to access and modify submission properties such as 'state' and 'form_id'. Eg: Store all submissions unpublished by default. * * We may support this event in the PHP Scripts section too. */ JFactory::getApplication()->triggerEvent('onConvertFormsSubmissionBeforeSave', [&$data]); $form_data = Form::load($data['form_id']); $submission = null; // If we are not saving the data to the database, mock the submission in order to be used as expected. if (isset($form_data['params']['save_data_to_db']) && $form_data['params']['save_data_to_db'] == '0') { $submission = (object) array_merge($form_data, $data); $submission->id = 'unsaved_submission_' . uniqid(); $submission->user_id = 0; $submission->created = JFactory::getDate()->toSql(); $submission->modified = JFactory::getDbo()->getNullDate(); $this->prepareItem($submission); JPluginHelper::importPlugin('convertforms'); JPluginHelper::importPlugin('convertformstools'); JFactory::getApplication()->triggerEvent('onConvertFormsConversionAfterSave', [$submission, $this, 1]); } else { // JSON_UNESCAPED_UNICODE encodes multibyte unicode characters literally. // Without: Τάσος => \u03a4\u03ac\u03c3\u03bf\u03c2 // With: Τάσος => Τάσος $data['params'] = json_encode($data['params'], JSON_UNESCAPED_UNICODE); // Everything seems fine. Let's save data to the database. if (!$this->save($data)) { throw new Exception($this->getError()); } $submission = $this->getItem(); } // Run user's PHP script after the form has been processed, stored into the database and all addons have run. // @todo - Move PHP Scripts logic into a separate plugin. $payload = ['submission' => &$submission]; Form::runPHPScript($data['form_id'], 'afterformsubmission', $payload); /** * Why this event was created: * * - Due to the fact that we cannot hook into "onConvertFormsSubmissionAfterSave" and manipulate the $submission object. * If we try to do so, the updated $submission wont find its way to the other listeners hooked to this event. * * When does this event run: * * - Right after the submission has been saved to the database. * - Before other plugins hook to run their own code after the submission has been processed. * * When it should be used: * * - When we want to modify the submission object just before other listeners hook into "onConvertFormsSubmissionAfterSave" * such as Email Notifications so they will get the updated $submission object. * * Example Use Case: * * - If we want to rename the uploaded files and use add the submission id to the filename, we must listen to this event, * update the $submission object, rename the file, update the database and emails will use the updated file data. */ JFactory::getApplication()->triggerEvent('onConvertFormsSubmissionAfterSavePrepare', [&$submission]); /** * When does this event run: * * - At the end of the submission process, after the $submission object has been finalized and its ready to be given * out to plugins/code that hook to this event. * * When it should be used: * * - We dont want to modify the $submission data and plan to execute our code after the form has been submitted * successfully. * * Example Use Case: * - Send Email Notifications * - Submit data to Campaigns */ JFactory::getApplication()->triggerEvent('onConvertFormsSubmissionAfterSave', [&$submission]); return $submission; } /** * Get a conversion item * * @param interger $pk The conversion row primary key * @param bool $prepare Whether to prepare the submission fields. * * @return object The conversion object */ public function getItem($pk = null, $prepare = true) { if (!$item = parent::getItem($pk)) { return; } $this->prepareItem($item, $prepare); return $item; } /** * Prepares the submission item. * * @param object $item The submission item. * @param bool $prepare Whether to prepare the submission fields. * * @return void */ protected function prepareItem(&$item, $prepare = true) { JPluginHelper::importPlugin('convertformstools'); // There's no need to change the timezone on the backend as the date properties are mainly used in the Calendar fields // which are already modifying the timezone offset with the filter="user_utc" property. if (JFactory::getApplication()->isClient('site')) { $item->created = Helper::formatDate($item->created); $item->modified = Helper::formatDate($item->modified); } if ($item->user_id) { $item->user_name = JFactory::getUser($item->user_id)->name; } // Load Form & Campaign Model JModelLegacy::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_convertforms/models', 'ConvertFormsModel'); $modelForm = JModelLegacy::getInstance('Form', 'ConvertFormsModel', ['ignore_request' => true]); $modelCampaign = JModelLegacy::getInstance('Campaign', 'ConvertFormsModel', ['ignore_request' => true]); $item->form = $modelForm->getItem($item->form_id); $item->campaign = $modelCampaign->getItem($item->campaign_id); // On J4 we get infinite loop if we inject the prepared fields to $data. // Thus, we don't prepare the submission on this method in order to not pollute the $data object. // Instead we use the preprocessForm() method with a cloned object. if ($prepare) { $this->prepare($item); } return $item; } /** * Prepares the submission by adding the submitted fields values. * * @param object $submission * * @return void */ public function prepare(&$submission) { if (!$submission || is_null($submission->id)) { return; } // Note: Form may be already available in the $submission object. if (!$form = Form::load($submission->form_id, false, true)) { return; } $fields = []; // Make sure we're manipualating an array. $submission_params = array_change_key_case((array) $submission->params); foreach ($form['fields'] as $field) { // Skip fields with no name like reCAPTCHA, HTML e.t.c if (!isset($field['name'])) { continue; } $field_name = strtolower($field['name']); $submitted_value = isset($submission_params[$field_name]) ? $submission_params[$field_name] : ''; // Make sure the type of field is valid if (!$class = FieldsHelper::getFieldClass($field['type'], $field)) { continue; } $prepared_field = (object) [ 'options' => new Registry($field), 'class' => $class, 'label' => isset($field['label']) && !empty($field['label']) ? JText::_($field['label']) : $field['name'], 'value' => $class->prepareValue($submitted_value), 'value_html' => $class->prepareValueHTML($submitted_value), 'value_raw' => $submitted_value ]; $fields[$field_name] = $prepared_field; } // @todo - Stop using 'prepare_fields' property and switch over to the standard 'fields' property. $submission->prepared_fields = $fields; } }