store = Store::getInstance(); $this->dbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP); if ($this->dbIndexData === false) { $this->dbIndexData = DB_INDEX_DEFAULT; // Fallback for FORMs which are not called via SIP; } $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData); $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM); if ($this->dbIndexQfq != $this->dbIndexData) { $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq); } $this->feSpecNative = $this->loadFormElementsBasedOnSIP(); $form = $this->store->getVar(SIP_FORM, STORE_SIP, SANITIZE_ALLOW_ALNUMX); if (!empty($form) && !defined('PHPUNIT_QFQ')) { // To make STORE_RECORD available at a very early stage. $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP, SANITIZE_ALLOW_DIGIT); $tableFromFormSql = "SELECT `tableName`, `primaryKey` FROM `Form` WHERE `name`=?"; $form = $this->dbArray[$this->dbIndexQfq]->sql($tableFromFormSql, ROW_EXPECT_1, [$form]); if (empty($form[F_PRIMARY_KEY])) { $form[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT; } $this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]); } $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]); } /** * Loads a minimal definition of FormElement of the form specified in SIP. * * @return array * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function loadFormElementsBasedOnSIP() { $formName = $this->store->getVar(SIP_FORM, STORE_SIP); // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM); $feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER, ROW_REGULAR, [$formName], 'Form or FormElements not found: ' . ERROR_FORM_NOT_FOUND); HelperFormElement::explodeParameterInArrayElements($feSpecNative, FE_PARAMETER); $feSpecTemplateGroup = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_CONTAINER_TEMPLATE_GROUP, ROW_REGULAR, [$formName]); HelperFormElement::explodeParameterInArrayElements($feSpecTemplateGroup, FE_PARAMETER); $feSpecNative = $this->expandTemplateGroupFormElement($feSpecTemplateGroup, $feSpecNative); return $feSpecNative; } /** * Checks if there are templateGroups defined. If yes, expand them. Return expanded feSpecNative array. * * @param array $feSpecTemplateGroup * @param array $feSpecNative * * @return array */ private function expandTemplateGroupFormElement(array $feSpecTemplateGroup, array $feSpecNative) { $expanded = array(); if (count($feSpecTemplateGroup) == 0) { return $feSpecNative; // No templateGroups >> nothing to do >> just return } // Iterate over all 'FormElements': part of a templateGroup? foreach ($feSpecNative as $fe) { $flagCopied = false; if ($fe[FE_ID_CONTAINER] > 0) { // Search for a corresponding template group. foreach ($feSpecTemplateGroup as $templateGroup) { if ($fe[FE_ID_CONTAINER] == $templateGroup[FE_ID]) { $flagCopied = true; // Get max copies per template group $maxCopies = HelperFormElement::tgGetMaxLength($templateGroup[FE_MAX_LENGTH]); // Copy each native FormElement $template = $fe[FE_NAME]; for ($ii = 1; $ii <= $maxCopies; $ii++) { $fe[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $template); $expanded[] = $fe; } } } } if (!$flagCopied) { $expanded[] = $fe; } } return $expanded; } /** * Copies all current form parameter from STORE_CLIENT to STORE_FORM. Checks the values against FormElement * definition and throws an exception if check fails. FormElements.type=hidden will be taken from STORE_SIP. * * @param string $formMode * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ public function process($formMode = FORM_SAVE) { // The following will never be used during load (fe.type='upload'). $skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER, FE_VALUE, FE_FILL_STORE_VAR, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT]; $html = ''; $newValues = array(); $clientValues = $this->store->getStore(STORE_CLIENT); $formModeGlobal = Support::getFormModeGlobal($this->formSpec[F_MODE_GLOBAL] ?? ''); if ($formMode == FORM_UPDATE && $formModeGlobal == '') { # During 'update': fake all elements to be not 'required'. $formModeGlobal = F_MODE_REQUIRED_OFF; } // If called through 'api/...': get STORE_TYPO3 via SIP parameter. if (isset($clientValues[CLIENT_TYPO3VARS]) && $formMode != FORM_REST) { $this->store->fillTypo3StoreFromSip($clientValues[CLIENT_TYPO3VARS]); } // Retrieve SIP vars, e.g. for HIDDEN elements. $sipValues = $this->store->getStore(STORE_SIP); // Copy SIP Values; not necessarily defined as a FormElement. foreach ($sipValues as $key => $value) { switch ($key) { case SIP_SIP: case SIP_RECORD_ID: case SIP_FORM: case SIP_TABLE: case SIP_URLPARAM: case 'id': break; default: $newValues[$key] = $value; break; } } if ($formMode != FORM_REST) { // Check if there is a 'new record already saved' situation: // yes: the names of the input fields are submitted with ':0' instead of ':' // no: regular situation, take real 'recordid' $fakeRecordId = isset($sipValues[SIP_MAKE_URLPARAM_UNIQ]) ? 0 : $sipValues[SIP_RECORD_ID]; } // Iterate over all FormElements. Sanitize values. Built an assoc array $newValues. foreach ($this->feSpecNative AS $formElement) { // Never get a predefined 'id' if ($formElement[FE_NAME] === COLUMN_ID) { continue; } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); // Evaluate current FormElement: e.g. FE_MODE_SQL $formElement = $this->evaluate->parseArray($formElement, $skip, $debugStack); // Get related formElement. Construct the field name used in the form. $clientFieldName = ($formMode == FORM_REST) ? $formElement[FE_NAME] : HelperFormElement::buildFormElementName($formElement, $fakeRecordId); // Some Defaults $formElement = Support::setFeDefaults($formElement, [FE_MODE => $formModeGlobal]); if ($formElement[FE_TYPE] === FE_TYPE_EXTRA) { // Extra elements will be transferred by SIP if (!isset($sipValues[$formElement[FE_NAME]])) { # Check for reserved names. if ($formElement[FE_NAME] == CLIENT_PAGE_ID || $formElement[FE_NAME] == CLIENT_PAGE_TYPE || $formElement[FE_NAME] == CLIENT_PAGE_LANGUAGE) { throw new \UserFormException( json_encode( [ERROR_MESSAGE_TO_USER => 'Reserved name "' . $formElement[FE_NAME] . '" in FormElement.', ERROR_MESSAGE_TO_DEVELOPER => 'FE_TYPE="extra" should not use ' . CLIENT_PAGE_ID . ',' . CLIENT_PAGE_TYPE . ',' . CLIENT_PAGE_LANGUAGE]), ERROR_FORM_RESERVED_NAME); } throw new \CodeException("Missing the " . FE_TYPE_EXTRA . " field '" . $formElement[FE_NAME] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP); } $newValues[$formElement[FE_NAME]] = $sipValues[$formElement[FE_NAME]] ?? ''; continue; } switch ($formElement[FE_TYPE]) { case FE_TYPE_CHECKBOX: // Checkbox Multi: collect values $val = $this->collectCheckBoxValues($clientFieldName, $clientValues, $formElement[FE_CHECKBOX_CHECKED] ?? ''); if ($val !== false) { $clientValues[$clientFieldName] = $val; } break; case FE_TYPE_ANNOTATE: $formElement[FE_ENCODE] = FE_ENCODE_NONE; break; default: break; } // Bug #5077 / 'Required' FormElement with Dynamic Update - required FE will be checked later - at this point there is no F, R store. // if ($formElement[FE_MODE] === FE_MODE_REQUIRED) { // if (!isset($clientValues[$clientFieldName]) || ($clientValues[$clientFieldName] === '')) { // throw new \UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY); // } // } // FORM_REST: typically form elements are filled and created on form load. This does not exist for REST Forms. // If a FE.value is defined, this has precedence over client supplied content. if ($formMode == FORM_REST && $formElement[FE_VALUE] != '') { $clientValues[$clientFieldName] = $this->evaluate->parse($formElement[FE_VALUE]); } // copy value to $newValues if (isset($clientValues[$clientFieldName])) { if ($formElement[FE_DYNAMIC_UPDATE] === 'yes' || $formElement[FE_MODE] === FE_MODE_REQUIRED || $formElement[FE_MODE] === FE_MODE_SHOW_REQUIRED || $formElement[FE_MODE] === FE_MODE_SHOW || (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] != '0')) { if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) { // TYPEAHEAD_TAG will be delivered as JSON. Check and sanitize every key/value pair. $arr = json_decode($clientValues[$clientFieldName], true); $arrTmp = array(); foreach ($arr as $row) { $arrKey = $this->doValue($formElement, $formMode, $row['key']); $arrValue = $this->doValue($formElement, $formMode, $row['value']); $arrTmp[$arrKey] = $arrValue; } $val = KeyValueStringParser::unparse($arrTmp); } else { // Single Value $val = $this->doValue($formElement, $formMode, $clientValues[$clientFieldName]); } $newValues[$formElement[FE_NAME]] = $val; } } } $this->store->setStore($newValues, STORE_FORM, true); } /** * @param $formElement * @param $value * @return string * @throws \CodeException * @throws \UserFormException */ private function doValue($formElement, $formMode, $value) { // Trim input if (empty($formElement[FE_TRIM])) { $value = trim($value); } elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) { $value = trim($value, $formElement[FE_TRIM]); } switch ($formElement[FE_TYPE]) { case FE_TYPE_DATE: case FE_TYPE_DATETIME: case FE_TYPE_TIME: if ($value !== '') { // do not check empty values $value = $this->doDateTime($formElement, $value); } break; default: if ($formElement[FE_TYPE] == FE_TYPE_EDITOR) { // Tiny MCE always wrap a '

' around the content. Remove it before saving. $value = Support::unWrapTag('

', $value); } // Check only if there is something. if ($value !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) { $value = Sanitize::sanitize($value, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN], $formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? ''); if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { // $value = htmlspecialchars($value, ENT_QUOTES); $value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value); } } break; } if ($value !== '') { $value = Sanitize::checkMinMax($value, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION); } return $value; } /** * Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma * separated string (MYSQL ENUM type). If there is no element '_h_${clientFieldName}', than there are no multi * values - return the already given `$clientValues[$clientFieldName]`. * * @param $clientFieldName * @param array $clientValues * * @return string */ private function collectCheckBoxValues($clientFieldName, array $clientValues, $unchecked) { // Check for Single // $checkboxKey = HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '', false); if (isset($clientValues[$clientFieldName])) { if (is_array($clientValues[$clientFieldName])) { return implode(',', $clientValues[$clientFieldName]); } return $clientValues[$clientFieldName]; } return $unchecked; // For templateGroups: all expanded FormElements will be tried to collect - this fails for not submitted fields. // Therefore skip not existing clientvalues. if (!isset($clientValues[$checkboxKey])) { return false; } // Check if there is a hidden value with naming in checkbox multi syntax if (isset($clientValues[$checkboxKey])) { $checkboxValue = $clientValues[$checkboxKey]; $pattern = '/' . HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '\d+') . '/'; foreach ($clientValues as $key => $value) { if (1 === preg_match($pattern, $key)) { $checkboxValue .= ',' . $value; } } if (isset($checkboxValue[0]) && $checkboxValue[0] === ',') { $checkboxValue = substr($checkboxValue, 1); } $clientValues[$clientFieldName] = $checkboxValue; } return $clientValues[$clientFieldName]; } /** * Check $value as date/datetime/time value and convert it to FORMAT_DATE_INTERNATIONAL. * * @param array $formElement - if not set, set $formElement[FE_DATE_FORMAT] * @param string $value - date/datetime/time value in format FORMAT_DATE_INTERNATIONAL or FORMAT_DATE_GERMAN * * @return string - checked datetime string * @throws \UserFormException */ public function doDateTime(array &$formElement, $value) { $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT], $formElement[FE_TIME_IS_OPTIONAL] ?? ""); if (1 !== preg_match('/' . $regexp . '/', $value, $matches)) { $placeholder = Support::getDateTimePlaceholder($formElement); throw new \UserFormException("DateTime format not recognized: $placeholder / $value ", ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED); } $showTime = $formElement[FE_TYPE] == FE_TYPE_DATE ? '0' : '1'; $value = Support::convertDateTime($value, FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]); if ($formElement[FE_TYPE] !== FE_TYPE_TIME) { // Validate date (e.g. 2010-02-31) $dateValue = explode(' ', $value)[0]; $dateParts = explode('-', $dateValue); if (!checkdate($dateParts[1], $dateParts[2], $dateParts[0])) throw new \UserFormException("$dateValue is not a valid date.", ERROR_INVALID_DATE); } return $value; } }