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

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