/** * @var Store */ protected $store = null; /** * @var Evaluate */ protected $evaluate = null; /** * @var string */ private $formId = null; /** * @var Sip */ private $sip = null; /** * AbstractBuildForm constructor. * * @param array $formSpec * @param array $feSpecAction * @param array $feSpecNative */ public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) { $this->formSpec = $formSpec; $this->feSpecAction = $feSpecAction; $this->feSpecNative = $feSpecNative; $this->store = Store::getInstance(); $this->db = new Database(); $this->evaluate = new Evaluate($this->store, $this->db); $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes'); $this->sip = $this->store->getSipInstance(); // render mode specific $this->fillWrap(); $this->buildElementFunctionName = [ 'checkbox' => 'Checkbox', 'date' => 'DateTime', 'datetime' => 'DateTime', 'dateJQW' => 'DateJQW', 'datetimeJQW' => 'DateJQW', 'email' => 'Input', 'gridJQW' => 'GridJQW', FE_TYPE_EXTRA => 'Extra', 'text' => 'Input', 'editor' => 'Editor', 'time' => 'DateTime', 'note' => 'Note', 'password' => 'Input', 'radio' => 'Radio', 'select' => 'Select', 'subrecord' => 'Subrecord', 'upload' => 'File', 'fieldset' => 'Fieldset', 'pill' => 'Pill', 'templateGroup' => 'TemplateGroup' ]; $this->buildRowName = [ 'checkbox' => 'Native', 'date' => 'Native', 'datetime' => 'Native', 'dateJQW' => 'Native', 'datetimeJQW' => 'Native', 'email' => 'Native', 'gridJQW' => 'Native', FE_TYPE_EXTRA => 'Native', 'text' => 'Native', 'editor' => 'Native', 'time' => 'Native', 'note' => 'Native', 'password' => 'Native', 'radio' => 'Native', 'select' => 'Native', 'subrecord' => 'Subrecord', 'upload' => 'Native', 'fieldset' => 'Fieldset', 'pill' => 'Pill', 'templateGroup' => 'TemplateGroup' ]; $this->symbol[SYMBOL_EDIT] = ""; $this->symbol[SYMBOL_NEW] = ""; $this->symbol[SYMBOL_DELETE] = ""; $this->inputCheckPattern = Sanitize::inputCheckPatternArray(); } abstract public function fillWrap(); /** * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'. * * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string|array $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes values/states * @throws CodeException * @throws DbException * @throws \qfq\UserFormException */ public function process($mode, $htmlElementNameIdZero = false) { $htmlHead = ''; $htmlTail = ''; $htmlT3vars = ''; $htmlSubrecords = ''; $htmlElements = ''; $json = array(); $modeCollectFe = FLAG_DYNAMIC_UPDATE; $storeUse = STORE_USE_DEFAULT; if ($mode === FORM_SAVE) { $modeCollectFe = FLAG_ALL; $storeUse = STORE_RECORD . STORE_TABLE_DEFAULT; } //
if ($mode === FORM_LOAD) { $htmlHead = $this->head(); } $filter = $this->getProcessFilter(); if ($this->formSpec['multiMode'] !== 'none') { $parentRecords = $this->db->sql($this->formSpec['multiSql']); foreach ($parentRecords as $row) { $this->store->setStore($row, STORE_PARENT_RECORD, true); $jsonTmp = array(); $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe); $json[] = $jsonTmp; } } else { $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode); } // if ($mode === FORM_LOAD) { $htmlT3vars = $this->prepareT3VarsForSave(); $htmlTail = $this->tail(); $htmlSubrecords = $this->doSubrecords(); } $htmlHidden = $this->buildAdditionalFormElements(); $htmlSip = $this->buildHiddenSip($json); return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail . $htmlSubrecords : $json; } /** * Builds the head area of the form. * * @return string */ public function head() { $html = ''; $html .= '
formSpec[F_CLASS], TRUE) . '>'; // main
around everything // Logged in BE User will see a FormEdit Link $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'"); $formEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM, $this->formSpec[F_ID]); $html .= "

Edit [$sipParamString]

"; $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec[F_TITLE], true); $html .= $this->getFormTag(); return $html; } /** * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP. * * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT * @param int $recordId id of form or formElement * @param array $param * @return string String: Edit [sip:..., r:..., urlparam:..., ...] * @throws CodeException */ public function createFormEditorUrl($form, $recordId, array $param = array()) { if (!$this->showDebugInfo) { return ''; } $queryStringArray = [ 'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM), 'form' => $form, 'r' => $recordId ]; $queryStringArray = array_merge($queryStringArray, $param); $queryString = Support::arrayToQueryString($queryStringArray); $sip = $this->store->getSipInstance(); $url = $sip->queryStringToSip($queryString); return $url; } /** * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''. * * @param $item * @param $value * @param bool|false $flagOmitEmpty * @return string */ public function wrapItem($item, $value, $flagOmitEmpty = false) { if ($flagOmitEmpty && $value === "") { return ''; } return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END]; } /** * Returns ''-tag with various attributes. * * @return string */ public function getFormTag() { $attribute = $this->getFormTagAtrributes(); $honeypot = $this->getHoneypotVars(); return '' . $honeypot; } /** * Create HTML Input vars to detect bot automatic filling of forms. * * @return string */ public function getHoneypotVars() { $html = ''; $vars = $this->store->getVar(SYSTEM_SECURITY_VARS_HONEYPOT, STORE_SYSTEM); // Iterate over all fake vars $arr = explode(',', $vars); foreach ($arr as $name) { $name = trim($name); if ($name === '') { continue; } $html .= ""; } return $html; } /** * Build an assoc array with standard form attributes. * * @return array */ public function getFormTagAtrributes() { $attribute['id'] = $this->getFormId(); $attribute['method'] = 'post'; $attribute['action'] = $this->getActionUrl(); $attribute['target'] = '_top'; $attribute['accept-charset'] = 'UTF-8'; $attribute['autocomplete'] = 'on'; $attribute['enctype'] = $this->getEncType(); return $attribute; } /** * Return a uniq form id * * @return string */ public function getFormId() { if ($this->formId === null) { $this->formId = uniqid('qfq-form-'); } return $this->formId; } /** * Builds the HTML 'form'-tag inlcuding all attributes and target. * * Notice: the SIP will be transferred as POST Parameter. * * @return string * @throws DbException */ public function getActionUrl() { return API_DIR . '/save.php'; } /** * Determines the enctype. * * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes * * @return string * @throws DbException */ public function getEncType() { $result = $this->db->sql("SELECT id FROM FormElement AS fe WHERE fe.formId=? AND fe.type='upload' LIMIT 1", ROW_REGULAR, [$this->formSpec['id']], 'Look for Formelement.type="upload"'); return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded'; } abstract public function getProcessFilter(); /** * Process all FormElements: Collect and return all HTML code & JSON. * * @param $recordId * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD * @param int $feIdContainer * @param array $json * @param string $modeCollectFe * @param bool $htmlElementNameIdZero * @param string $storeUseDefault * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE * @return string * @throws CodeException * @throws DbException * @throws \qfq\UserFormException */ public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json, $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) { $html = ''; // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded. $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER]; // get current data record if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) { $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1, array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" . $this->formSpec[F_TABLE_NAME] . "'."); $this->store->setStore($row, STORE_RECORD); } $this->checkAutoFocus(); // Iterate over all FormElements foreach ($this->feSpecNative as $fe) { $storeUse = $storeUseDefault; if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord') || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord') // || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no') ) { continue; // skip this FE } $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form $debugStack = array(); // Copy global readonly mode. if ($this->formSpec[F_MODE] == F_MODE_READONLY) { $fe[FE_MODE] = FE_MODE_READONLY; $fe[FE_MODE_SQL] = ''; } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); // Fill STORE_LDAP $fe = $this->prepareFillStoreFireLdap($fe); // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'. if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) { Support::setIfNotSet($fe, FE_SLAVE_ID); $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID])); $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR); } // ** evaluate current FormElement ** $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack); // Some Defaults $formElement = Support::setFeDefaults($formElement, $this->formSpec); if ($flagOutput === true) { $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]); } //In case the current element is a 'RETYPE' element: take the element name of the source FormElement. Needed in the next row to retrieve the default value. $name = (isset($formElement[FE_RETYPE_SOURCE_NAME])) ? $formElement[FE_RETYPE_SOURCE_NAME] : $formElement[FE_NAME]; $value = ''; Support::setIfNotSet($formElement, FE_VALUE); if (is_array($formElement[FE_VALUE])) { $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : ''; } // If is FormElement['value'] explicit defined: take it // There are two options: a) single value, b) array of values (template Group) // if (is_array($formElement[FE_VALUE])) { // // For Templates Groups, the 'value' has to be defined as '{{!SELECT ...' wich returns all selected records in an array. // $idx = isset($formElement[FE_TEMPLATE_GROUP_CURRENT_IDX]) ? $formElement[FE_TEMPLATE_GROUP_CURRENT_IDX] - 1 : 0; // if (isset($formElement[FE_VALUE][$idx]) && is_array($formElement[FE_VALUE][$idx])) { // $value = current($formElement[FE_VALUE][$idx]); // if ($value === false) { // $value = ''; // } // } // } else { $value = $formElement[FE_VALUE]; // } if ($value === '') { // #2064 / Only take the default, if the FE is a real tablecolumn. // #3426 / Dynamic Update: Inputs loose the new content and shows the old value. if ($storeUse == STORE_USE_DEFAULT && $this->store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) === false) { $storeUse = str_replace(STORE_TABLE_DEFAULT, '', $storeUse); // Remove STORE_DEFAULT } // Retrieve value via FSRVD $value = $this->store->getVar($name, $storeUse, $formElement[FE_CHECK_TYPE], $foundInStore); if ($foundInStore == STORE_RECORD && $formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { $value = htmlspecialchars_decode($value, ENT_QUOTES); } } // Typically: $htmlElementNameIdZero = true // After Saving a record, staying on the form, the FormElements on the Client are still known as ':0'. $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId); $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID], ($htmlElementNameIdZero) ? 0 : $recordId, 0); // Construct Marshaller Name: buildElement $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]]; $jsonElement = array(); $elementExtra = ''; // Render pure element $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode); // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements. if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) { if (count($jsonElement) > 0) { $json = array_merge($json, $jsonElement); } } else { // for non container elements: just add the current json status if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] == 'yes')) { if (isset($jsonElement[0]) && is_array($jsonElement[0])) { // Checkboxes are delivered as array of arrays: unnest them and append them to the existing json array. $json = array_merge($json, $jsonElement); } else { $json[] = $jsonElement; } } } if ($flagOutput) { // debugStack as Tooltip if ($this->showDebugInfo) { if (count($debugStack) > 0) { $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack)); } // Build 'FormElement' Edit symbol $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]); $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']'); $icon = Support::wrapTag('', ''); $elementHtml .= Support::wrapTag("