vendor/contao/core-bundle/src/Resources/contao/forms/Form.php line 96

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. /**
  11.  * Provide methods to handle front end forms.
  12.  *
  13.  * @property integer $id
  14.  * @property string  $title
  15.  * @property string  $formID
  16.  * @property string  $method
  17.  * @property boolean $allowTags
  18.  * @property string  $attributes
  19.  * @property boolean $novalidate
  20.  * @property integer $jumpTo
  21.  * @property boolean $sendViaEmail
  22.  * @property boolean $skipEmpty
  23.  * @property string  $format
  24.  * @property string  $recipient
  25.  * @property string  $subject
  26.  * @property boolean $storeValues
  27.  * @property string  $targetTable
  28.  * @property string  $customTpl
  29.  */
  30. class Form extends Hybrid
  31. {
  32.     /**
  33.      * Model
  34.      * @var FormModel
  35.      */
  36.     protected $objModel;
  37.     /**
  38.      * Key
  39.      * @var string
  40.      */
  41.     protected $strKey 'form';
  42.     /**
  43.      * Table
  44.      * @var string
  45.      */
  46.     protected $strTable 'tl_form';
  47.     /**
  48.      * Template
  49.      * @var string
  50.      */
  51.     protected $strTemplate 'form_wrapper';
  52.     /**
  53.      * Remove name attributes in the back end so the form is not validated
  54.      *
  55.      * @return string
  56.      */
  57.     public function generate()
  58.     {
  59.         $request System::getContainer()->get('request_stack')->getCurrentRequest();
  60.         if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request))
  61.         {
  62.             $objTemplate = new BackendTemplate('be_wildcard');
  63.             $objTemplate->wildcard '### ' $GLOBALS['TL_LANG']['CTE']['form'][0] . ' ###';
  64.             $objTemplate->id $this->id;
  65.             $objTemplate->link $this->title;
  66.             $objTemplate->href StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', array('do'=>'form''table'=>'tl_form_field''id'=>$this->id)));
  67.             return $objTemplate->parse();
  68.         }
  69.         if ($this->customTpl)
  70.         {
  71.             $request System::getContainer()->get('request_stack')->getCurrentRequest();
  72.             // Use the custom template unless it is a back end request
  73.             if (!$request || !System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request))
  74.             {
  75.                 $this->strTemplate $this->customTpl;
  76.             }
  77.         }
  78.         return parent::generate();
  79.     }
  80.     /**
  81.      * Generate the form
  82.      */
  83.     protected function compile()
  84.     {
  85.         $hasUpload false;
  86.         $doNotSubmit false;
  87.         $arrSubmitted = array();
  88.         $this->loadDataContainer('tl_form_field');
  89.         $formId $this->formID 'auto_' $this->formID 'auto_form_' $this->id;
  90.         $this->Template->fields '';
  91.         $this->Template->hidden '';
  92.         $this->Template->formSubmit $formId;
  93.         $this->Template->method = ($this->method == 'GET') ? 'get' 'post';
  94.         $this->Template->requestToken System::getContainer()->get('contao.csrf.token_manager')->getDefaultTokenValue();
  95.         $this->initializeSession($formId);
  96.         $arrLabels = array();
  97.         // Get all form fields
  98.         $arrFields = array();
  99.         $objFields FormFieldModel::findPublishedByPid($this->id);
  100.         if ($objFields !== null)
  101.         {
  102.             while ($objFields->next())
  103.             {
  104.                 // Ignore the name of form fields which do not use a name (see #1268)
  105.                 if ($objFields->name && isset($GLOBALS['TL_DCA']['tl_form_field']['palettes'][$objFields->type]) && preg_match('/[,;]name[,;]/'$GLOBALS['TL_DCA']['tl_form_field']['palettes'][$objFields->type]))
  106.                 {
  107.                     $arrFields[$objFields->name] = $objFields->current();
  108.                 }
  109.                 else
  110.                 {
  111.                     $arrFields[] = $objFields->current();
  112.                 }
  113.             }
  114.         }
  115.         // HOOK: compile form fields
  116.         if (isset($GLOBALS['TL_HOOKS']['compileFormFields']) && \is_array($GLOBALS['TL_HOOKS']['compileFormFields']))
  117.         {
  118.             foreach ($GLOBALS['TL_HOOKS']['compileFormFields'] as $callback)
  119.             {
  120.                 $this->import($callback[0]);
  121.                 $arrFields $this->{$callback[0]}->{$callback[1]}($arrFields$formId$this);
  122.             }
  123.         }
  124.         // Process the fields
  125.         if (!empty($arrFields) && \is_array($arrFields))
  126.         {
  127.             $row 0;
  128.             $max_row = \count($arrFields);
  129.             foreach ($arrFields as $objField)
  130.             {
  131.                 /** @var FormFieldModel $objField */
  132.                 $strClass $GLOBALS['TL_FFL'][$objField->type] ?? null;
  133.                 // Continue if the class is not defined
  134.                 if (!class_exists($strClass))
  135.                 {
  136.                     continue;
  137.                 }
  138.                 $arrData $objField->row();
  139.                 $arrData['decodeEntities'] = true;
  140.                 $arrData['allowHtml'] = $this->allowTags;
  141.                 $arrData['rowClass'] = 'row_' $row . (($row == 0) ? ' row_first' : (($row == ($max_row 1)) ? ' row_last' '')) . ((($row 2) == 0) ? ' even' ' odd');
  142.                 // Increase the row count if it's a password field
  143.                 if ($objField->type == 'password')
  144.                 {
  145.                     ++$row;
  146.                     ++$max_row;
  147.                     $arrData['rowClassConfirm'] = 'row_' $row . (($row == ($max_row 1)) ? ' row_last' '') . ((($row 2) == 0) ? ' even' ' odd');
  148.                 }
  149.                 // Submit buttons do not use the name attribute
  150.                 if ($objField->type == 'submit')
  151.                 {
  152.                     $arrData['name'] = '';
  153.                 }
  154.                 // Unset the default value depending on the field type (see #4722)
  155.                 if (!empty($arrData['value']) && !\in_array('value'StringUtil::trimsplit('[,;]'$GLOBALS['TL_DCA']['tl_form_field']['palettes'][$objField->type] ?? '')))
  156.                 {
  157.                     $arrData['value'] = '';
  158.                 }
  159.                 /** @var Widget $objWidget */
  160.                 $objWidget = new $strClass($arrData);
  161.                 $objWidget->required $objField->mandatory true false;
  162.                 // HOOK: load form field callback
  163.                 if (isset($GLOBALS['TL_HOOKS']['loadFormField']) && \is_array($GLOBALS['TL_HOOKS']['loadFormField']))
  164.                 {
  165.                     foreach ($GLOBALS['TL_HOOKS']['loadFormField'] as $callback)
  166.                     {
  167.                         $this->import($callback[0]);
  168.                         $objWidget $this->{$callback[0]}->{$callback[1]}($objWidget$formId$this->arrData$this);
  169.                     }
  170.                 }
  171.                 // Validate the input
  172.                 if (Input::post('FORM_SUBMIT') == $formId)
  173.                 {
  174.                     $objWidget->validate();
  175.                     // HOOK: validate form field callback
  176.                     if (isset($GLOBALS['TL_HOOKS']['validateFormField']) && \is_array($GLOBALS['TL_HOOKS']['validateFormField']))
  177.                     {
  178.                         foreach ($GLOBALS['TL_HOOKS']['validateFormField'] as $callback)
  179.                         {
  180.                             $this->import($callback[0]);
  181.                             $objWidget $this->{$callback[0]}->{$callback[1]}($objWidget$formId$this->arrData$this);
  182.                         }
  183.                     }
  184.                     if ($objWidget->hasErrors())
  185.                     {
  186.                         $doNotSubmit true;
  187.                     }
  188.                     // Store current value in the session
  189.                     elseif ($objWidget->submitInput())
  190.                     {
  191.                         $arrSubmitted[$objField->name] = $objWidget->value;
  192.                         $_SESSION['FORM_DATA'][$objField->name] = $objWidget->value;
  193.                         unset($_POST[$objField->name]); // see #5474
  194.                     }
  195.                 }
  196.                 if ($objWidget instanceof UploadableWidgetInterface)
  197.                 {
  198.                     $hasUpload true;
  199.                 }
  200.                 if ($objWidget instanceof FormHidden)
  201.                 {
  202.                     $this->Template->hidden .= $objWidget->parse();
  203.                     --$max_row;
  204.                     continue;
  205.                 }
  206.                 if ($objWidget->name && $objWidget->label)
  207.                 {
  208.                     $arrLabels[$objWidget->name] = System::getContainer()->get('contao.insert_tag.parser')->replaceInline($objWidget->label); // see #4268
  209.                 }
  210.                 $this->Template->fields .= $objWidget->parse();
  211.                 ++$row;
  212.             }
  213.         }
  214.         // Process the form data
  215.         if (!$doNotSubmit && Input::post('FORM_SUBMIT') == $formId)
  216.         {
  217.             $this->processFormData($arrSubmitted$arrLabels$arrFields);
  218.         }
  219.         // Remove any uploads, if form did not validate (#1185)
  220.         if ($doNotSubmit && $hasUpload && !empty($_SESSION['FILES']))
  221.         {
  222.             foreach ($_SESSION['FILES'] as $field => $upload)
  223.             {
  224.                 if (empty($arrFields[$field]))
  225.                 {
  226.                     continue;
  227.                 }
  228.                 if (!empty($upload['uuid']) && null !== ($file FilesModel::findById($upload['uuid'])))
  229.                 {
  230.                     $file->delete();
  231.                 }
  232.                 if (is_file($upload['tmp_name']))
  233.                 {
  234.                     unlink($upload['tmp_name']);
  235.                 }
  236.                 unset($_SESSION['FILES'][$field]);
  237.             }
  238.         }
  239.         // Add a warning to the page title
  240.         if ($doNotSubmit && !Environment::get('isAjaxRequest'))
  241.         {
  242.             /** @var PageModel $objPage */
  243.             global $objPage;
  244.             $title $objPage->pageTitle ?: $objPage->title;
  245.             $objPage->pageTitle $GLOBALS['TL_LANG']['ERR']['form'] . ' - ' $title;
  246.         }
  247.         $strAttributes '';
  248.         $arrAttributes StringUtil::deserialize($this->attributestrue);
  249.         if (!empty($arrAttributes[0]))
  250.         {
  251.             $strAttributes .= ' id="' $arrAttributes[0] . '"';
  252.         }
  253.         if (!empty($arrAttributes[1]))
  254.         {
  255.             $strAttributes .= ' class="' $arrAttributes[1] . '"';
  256.         }
  257.         $this->Template->hasError $doNotSubmit;
  258.         $this->Template->attributes $strAttributes;
  259.         $this->Template->enctype $hasUpload 'multipart/form-data' 'application/x-www-form-urlencoded';
  260.         $this->Template->maxFileSize $hasUpload $this->objModel->getMaxUploadFileSize() : false;
  261.         $this->Template->novalidate $this->novalidate ' novalidate' '';
  262.         // Get the target URL
  263.         if ($this->method == 'GET' && ($objTarget $this->objModel->getRelated('jumpTo')) instanceof PageModel)
  264.         {
  265.             /** @var PageModel $objTarget */
  266.             $this->Template->action $objTarget->getFrontendUrl();
  267.         }
  268.     }
  269.     /**
  270.      * Process form data, store it in the session and redirect to the jumpTo page
  271.      *
  272.      * @param array $arrSubmitted
  273.      * @param array $arrLabels
  274.      * @param array $arrFields
  275.      */
  276.     protected function processFormData($arrSubmitted$arrLabels$arrFields)
  277.     {
  278.         // HOOK: prepare form data callback
  279.         if (isset($GLOBALS['TL_HOOKS']['prepareFormData']) && \is_array($GLOBALS['TL_HOOKS']['prepareFormData']))
  280.         {
  281.             foreach ($GLOBALS['TL_HOOKS']['prepareFormData'] as $callback)
  282.             {
  283.                 $this->import($callback[0]);
  284.                 $this->{$callback[0]}->{$callback[1]}($arrSubmitted$arrLabels$arrFields$this);
  285.             }
  286.         }
  287.         // Send form data via e-mail
  288.         if ($this->sendViaEmail)
  289.         {
  290.             $keys = array();
  291.             $values = array();
  292.             $fields = array();
  293.             $message '';
  294.             foreach ($arrSubmitted as $k=>$v)
  295.             {
  296.                 if ($k == 'cc')
  297.                 {
  298.                     continue;
  299.                 }
  300.                 $v StringUtil::deserialize($v);
  301.                 // Skip empty fields
  302.                 if ($this->skipEmpty && !\is_array($v) && !\strlen($v))
  303.                 {
  304.                     continue;
  305.                 }
  306.                 // Add field to message
  307.                 $message .= ($arrLabels[$k] ?? ucfirst($k)) . ': ' . (\is_array($v) ? implode(', '$v) : $v) . "\n";
  308.                 // Prepare XML file
  309.                 if ($this->format == 'xml')
  310.                 {
  311.                     $fields[] = array
  312.                     (
  313.                         'name' => $k,
  314.                         'values' => (\is_array($v) ? $v : array($v))
  315.                     );
  316.                 }
  317.                 // Prepare CSV file
  318.                 if ($this->format == 'csv' || $this->format == 'csv_excel')
  319.                 {
  320.                     $keys[] = $k;
  321.                     $values[] = (\is_array($v) ? implode(','$v) : $v);
  322.                 }
  323.             }
  324.             $recipients StringUtil::splitCsv($this->recipient);
  325.             // Format recipients
  326.             foreach ($recipients as $k=>$v)
  327.             {
  328.                 $recipients[$k] = str_replace(array('['']''"'), array('<''>'''), $v);
  329.             }
  330.             $email = new Email();
  331.             // Get subject and message
  332.             if ($this->format == 'email')
  333.             {
  334.                 $message $arrSubmitted['message'];
  335.                 $email->subject $arrSubmitted['subject'];
  336.             }
  337.             // Set the admin e-mail as "from" address
  338.             $email->from $GLOBALS['TL_ADMIN_EMAIL'];
  339.             $email->fromName $GLOBALS['TL_ADMIN_NAME'];
  340.             // Get the "reply to" address
  341.             if (!empty(Input::post('email'true)))
  342.             {
  343.                 $replyTo Input::post('email'true);
  344.                 // Add the name
  345.                 if (!empty(Input::post('name')))
  346.                 {
  347.                     $replyTo '"' Input::post('name') . '" <' $replyTo '>';
  348.                 }
  349.                 elseif (!empty(Input::post('firstname')) && !empty(Input::post('lastname')))
  350.                 {
  351.                     $replyTo '"' Input::post('firstname') . ' ' Input::post('lastname') . '" <' $replyTo '>';
  352.                 }
  353.                 $email->replyTo($replyTo);
  354.             }
  355.             // Fallback to default subject
  356.             if (!$email->subject)
  357.             {
  358.                 $email->subject html_entity_decode(System::getContainer()->get('contao.insert_tag.parser')->replaceInline($this->subject), ENT_QUOTES'UTF-8');
  359.             }
  360.             // Send copy to sender
  361.             if (!empty($arrSubmitted['cc']))
  362.             {
  363.                 $email->sendCc(Input::post('email'true));
  364.                 unset($_SESSION['FORM_DATA']['cc']);
  365.             }
  366.             // Attach XML file
  367.             if ($this->format == 'xml')
  368.             {
  369.                 $objTemplate = new FrontendTemplate('form_xml');
  370.                 $objTemplate->fields $fields;
  371.                 $objTemplate->charset System::getContainer()->getParameter('kernel.charset');
  372.                 $email->attachFileFromString($objTemplate->parse(), 'form.xml''application/xml');
  373.             }
  374.             // Attach CSV file
  375.             if ($this->format == 'csv')
  376.             {
  377.                 $email->attachFileFromString(StringUtil::decodeEntities('"' implode('";"'$keys) . '"' "\n" '"' implode('";"'$values) . '"'), 'form.csv''text/comma-separated-values');
  378.             }
  379.             elseif ($this->format == 'csv_excel')
  380.             {
  381.                 $email->attachFileFromString(mb_convert_encoding("\u{FEFF}sep=;\n" StringUtil::decodeEntities('"' implode('";"'$keys) . '"' "\n" '"' implode('";"'$values) . '"'), 'UTF-16LE''UTF-8'), 'form.csv''text/comma-separated-values');
  382.             }
  383.             $uploaded '';
  384.             // Attach uploaded files
  385.             if (!empty($_SESSION['FILES']))
  386.             {
  387.                 foreach ($_SESSION['FILES'] as $file)
  388.                 {
  389.                     // Add a link to the uploaded file
  390.                     if ($file['uploaded'])
  391.                     {
  392.                         $uploaded .= "\n" Environment::get('base') . StringUtil::stripRootDir(\dirname($file['tmp_name'])) . '/' rawurlencode($file['name']);
  393.                         continue;
  394.                     }
  395.                     $email->attachFileFromString(file_get_contents($file['tmp_name']), $file['name'], $file['type']);
  396.                 }
  397.             }
  398.             $uploaded trim($uploaded) ? "\n\n---\n" $uploaded '';
  399.             $email->text StringUtil::decodeEntities(trim($message)) . $uploaded "\n\n";
  400.             // Set the transport
  401.             if (!empty($this->mailerTransport))
  402.             {
  403.                 $email->addHeader('X-Transport'$this->mailerTransport);
  404.             }
  405.             // Send the e-mail
  406.             $email->sendTo($recipients);
  407.         }
  408.         // Store the values in the database
  409.         if ($this->storeValues && $this->targetTable)
  410.         {
  411.             $arrSet = array();
  412.             // Add the timestamp
  413.             if ($this->Database->fieldExists('tstamp'$this->targetTable))
  414.             {
  415.                 $arrSet['tstamp'] = time();
  416.             }
  417.             // Fields
  418.             foreach ($arrSubmitted as $k=>$v)
  419.             {
  420.                 if ($k != 'cc' && $k != 'id')
  421.                 {
  422.                     $arrSet[$k] = $v;
  423.                     // Convert date formats into timestamps (see #6827)
  424.                     if ($arrSet[$k] && \in_array($arrFields[$k]->rgxp, array('date''time''datim')))
  425.                     {
  426.                         $objDate = new Date($arrSet[$k], Date::getFormatFromRgxp($arrFields[$k]->rgxp));
  427.                         $arrSet[$k] = $objDate->tstamp;
  428.                     }
  429.                 }
  430.             }
  431.             // Files
  432.             if (!empty($_SESSION['FILES']))
  433.             {
  434.                 foreach ($_SESSION['FILES'] as $k=>$v)
  435.                 {
  436.                     if ($v['uploaded'] ?? null)
  437.                     {
  438.                         $arrSet[$k] = StringUtil::stripRootDir($v['tmp_name']);
  439.                     }
  440.                 }
  441.             }
  442.             // HOOK: store form data callback
  443.             if (isset($GLOBALS['TL_HOOKS']['storeFormData']) && \is_array($GLOBALS['TL_HOOKS']['storeFormData']))
  444.             {
  445.                 foreach ($GLOBALS['TL_HOOKS']['storeFormData'] as $callback)
  446.                 {
  447.                     $this->import($callback[0]);
  448.                     $arrSet $this->{$callback[0]}->{$callback[1]}($arrSet$this);
  449.                 }
  450.             }
  451.             // Load DataContainer of target table before trying to determine empty value (see #3499)
  452.             Controller::loadDataContainer($this->targetTable);
  453.             // Set the correct empty value (see #6284, #6373)
  454.             foreach ($arrSet as $k=>$v)
  455.             {
  456.                 if ($v === '')
  457.                 {
  458.                     $arrSet[$k] = Widget::getEmptyValueByFieldType($GLOBALS['TL_DCA'][$this->targetTable]['fields'][$k]['sql'] ?? array());
  459.                 }
  460.             }
  461.             // Do not use Models here (backwards compatibility)
  462.             $this->Database->prepare("INSERT INTO " $this->targetTable " %s")->set($arrSet)->execute();
  463.         }
  464.         // Store all values in the session
  465.         foreach (array_keys($_POST) as $key)
  466.         {
  467.             $_SESSION['FORM_DATA'][$key] = $this->allowTags Input::postHtml($keytrue) : Input::post($keytrue);
  468.         }
  469.         // Store the submission time to invalidate the session later on
  470.         $_SESSION['FORM_DATA']['SUBMITTED_AT'] = time();
  471.         $arrFiles $_SESSION['FILES'] ?? null;
  472.         // HOOK: process form data callback
  473.         if (isset($GLOBALS['TL_HOOKS']['processFormData']) && \is_array($GLOBALS['TL_HOOKS']['processFormData']))
  474.         {
  475.             foreach ($GLOBALS['TL_HOOKS']['processFormData'] as $callback)
  476.             {
  477.                 $this->import($callback[0]);
  478.                 $this->{$callback[0]}->{$callback[1]}($arrSubmitted$this->arrData$arrFiles$arrLabels$this);
  479.             }
  480.         }
  481.         $_SESSION['FILES'] = array(); // DO NOT CHANGE
  482.         // Add a log entry
  483.         if (System::getContainer()->get('contao.security.token_checker')->hasFrontendUser())
  484.         {
  485.             $this->import(FrontendUser::class, 'User');
  486.             System::getContainer()->get('monolog.logger.contao.forms')->info('Form "' $this->title '" has been submitted by "' $this->User->username '".');
  487.         }
  488.         else
  489.         {
  490.             System::getContainer()->get('monolog.logger.contao.forms')->info('Form "' $this->title '" has been submitted by a guest.');
  491.         }
  492.         // Check whether there is a jumpTo page
  493.         if (($objJumpTo $this->objModel->getRelated('jumpTo')) instanceof PageModel)
  494.         {
  495.             $this->jumpToOrReload($objJumpTo->row());
  496.         }
  497.         $this->reload();
  498.     }
  499.     /**
  500.      * Get the maximum file size that is allowed for file uploads
  501.      *
  502.      * @return integer
  503.      *
  504.      * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  505.      *             Use $this->objModel->getMaxUploadFileSize() instead.
  506.      */
  507.     protected function getMaxFileSize()
  508.     {
  509.         trigger_deprecation('contao/core-bundle''4.0''Using "Contao\Form::getMaxFileSize()" has been deprecated and will no longer work in Contao 5.0. Use "$this->objModel->getMaxUploadFileSize()" instead.');
  510.         return $this->objModel->getMaxUploadFileSize();
  511.     }
  512.     /**
  513.      * Initialize the form in the current session
  514.      *
  515.      * @param string $formId
  516.      */
  517.     protected function initializeSession($formId)
  518.     {
  519.         if (Input::post('FORM_SUBMIT') != $formId)
  520.         {
  521.             return;
  522.         }
  523.         $arrMessageBox = array('TL_ERROR''TL_CONFIRM''TL_INFO');
  524.         $_SESSION['FORM_DATA'] = \is_array($_SESSION['FORM_DATA'] ?? null) ? $_SESSION['FORM_DATA'] : array();
  525.         foreach ($arrMessageBox as $tl)
  526.         {
  527.             if (\is_array($_SESSION[$formId][$tl] ?? null))
  528.             {
  529.                 $_SESSION[$formId][$tl] = array_unique($_SESSION[$formId][$tl]);
  530.                 foreach ($_SESSION[$formId][$tl] as $message)
  531.                 {
  532.                     $objTemplate = new FrontendTemplate('form_message');
  533.                     $objTemplate->message $message;
  534.                     $objTemplate->class strtolower($tl);
  535.                     $this->Template->fields .= $objTemplate->parse() . "\n";
  536.                 }
  537.                 $_SESSION[$formId][$tl] = array();
  538.             }
  539.         }
  540.     }
  541. }
  542. class_alias(Form::class, 'Form');