/** * @var Store */ protected $store = null; /** * @var Evaluate */ protected $evaluate = null; /** * @var string */ private $formId = null; /** * @var Sip */ private $sip = null; /** * @var Link */ protected $link = null; /** * @var Report */ private $report = null; /** * @var BodytextParser */ private $bodytextParser = null; /** * @var Database[] Array of Database instantiated class */ protected $dbArray = array(); /** * @var bool|mixed */ protected $dbIndexData = false; /** * @var bool|string */ protected $dbIndexQfq = false; /** * AbstractBuildForm constructor. * * @param array $formSpec * @param array $feSpecAction * @param array $feSpecNative * @param array Database $db * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db = null) { $this->formSpec = $formSpec; $this->feSpecAction = $feSpecAction; $this->feSpecNative = $feSpecNative; $this->store = Store::getInstance(); // $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM); $this->dbIndexData = $formSpec[F_DB_INDEX]; $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM); $this->dbArray = $db; $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]); $this->showDebugInfoFlag = Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM)); $this->sip = $this->store->getSipInstance(); $this->link = new Link($this->sip, $this->dbIndexData); // render mode specific $this->fillWrap(); $this->buildElementFunctionName = [ FE_TYPE_CHECKBOX => 'Checkbox', FE_TYPE_DATE => 'DateTime', FE_TYPE_DATETIME => 'DateTime', 'dateJQW' => 'DateJQW', 'datetimeJQW' => 'DateJQW', 'email' => 'Input', 'gridJQW' => 'GridJQW', FE_TYPE_EXTRA => 'Extra', FE_TYPE_TEXT => 'Input', FE_TYPE_EDITOR => 'Editor', FE_TYPE_TIME => 'DateTime', FE_TYPE_NOTE => 'Note', FE_TYPE_PASSWORD => 'Input', FE_TYPE_RADIO => 'Radio', FE_TYPE_SELECT => 'Select', FE_TYPE_SUBRECORD => 'Subrecord', FE_TYPE_UPLOAD => 'File', FE_TYPE_ANNOTATE => 'Annotate', FE_TYPE_IMAGE_CUT => 'ImageCut', 'fieldset' => 'Fieldset', 'pill' => 'Pill', 'templateGroup' => 'TemplateGroup', ]; $this->buildRowName = [ FE_TYPE_CHECKBOX => 'Native', FE_TYPE_DATE => 'Native', FE_TYPE_DATETIME => 'Native', 'dateJQW' => 'Native', 'datetimeJQW' => 'Native', 'email' => 'Native', 'gridJQW' => 'Native', FE_TYPE_EXTRA => 'Native', FE_TYPE_TEXT => 'Native', FE_TYPE_EDITOR => 'Native', FE_TYPE_TIME => 'Native', FE_TYPE_NOTE => 'Native', FE_TYPE_PASSWORD => 'Native', FE_TYPE_RADIO => 'Native', FE_TYPE_SELECT => 'Native', FE_TYPE_SUBRECORD => 'Subrecord', FE_TYPE_UPLOAD => 'Native', FE_TYPE_ANNOTATE => 'Native', FE_TYPE_IMAGE_CUT => 'Native', 'fieldset' => 'Fieldset', 'pill' => 'Pill', 'templateGroup' => 'TemplateGroup', ]; $this->symbol[SYMBOL_EDIT] = ""; $this->symbol[SYMBOL_SHOW] = ""; $this->symbol[SYMBOL_NEW] = ""; $this->symbol[SYMBOL_DELETE] = ""; } 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 * * @param bool $htmlElementNameIdZero * @param array $latestFeSpecNative * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all * formElement.dynamicUpdate-yes values/states * @throws \CodeException * @throws \DbException * @throws \DownloadException * @throws \UserFormException * @throws \UserReportException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) { $htmlHead = ''; $htmlTail = ''; $htmlT3vars = ''; $htmlSubrecords = ''; $htmlElements = ''; $json = array(); // After action 'afterSave', it's necessary to reinitialize the FeSpecNative if (!empty($latestFeSpecNative)) { $this->feSpecNative = $latestFeSpecNative; } $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->dbArray[$this->dbIndexQfq]->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); if (!($recordId == '' || is_numeric($recordId))) { throw new \UserFormException( json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]), ERROR_INVALID_VALUE); } // Build FormElements $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode); if ($mode === FORM_SAVE && $recordId != 0) { // element-update: with 'value' $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO); $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]); // Via 'element-update' $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5; } } // 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. * * @param string $mode * @return string * @throws \CodeException * @throws \DbException * @throws \UserFormException */ public function head($mode = FORM_LOAD) { $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 * @throws \UserFormException */ public function createFormEditorUrl($form, $recordId, array $param = array()) { if (!$this->showDebugInfoFlag) { return ''; } $queryStringArray = [ 'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM), 'form' => $form, 'r' => $recordId, PARAM_DB_INDEX_DATA => $this->dbIndexQfq, ]; $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 string $item * @param string $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 * @throws \CodeException * @throws \DbException * @throws \UserFormException */ public function getFormTag() { $md5 = ''; $attribute = $this->getFormTagAttributes(); $honeypot = $this->getHoneypotVars(); $md5 = $this->buildInputRecordHashMd5(); return '' . $honeypot . $md5; } /** * Build MD5 from the current record. Return HTML Input element. * * @return string * @throws \CodeException * @throws \DbException * @throws \UserFormException */ public function buildInputRecordHashMd5() { $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO); $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]); $data = ""; // $data = ""; return $data; } /** * @param $tableName * @param $recordId * @param string $primaryKey * * @return string * @throws \CodeException * @throws \DbException * @throws \UserFormException */ public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) { $record = array(); if ($recordId != 0) { $record = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM $tableName WHERE $primaryKey=?", ROW_EXPECT_1, [$recordId], "Record to load not found."); } return OnArray::getMd5($record); } /** * Create HTML Input vars to detect bot automatic filling of forms. * * @return string * @throws \CodeException * @throws \UserFormException */ 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 * @throws \CodeException * @throws \DbException * @throws \UserFormException */ public function getFormTagAttributes() { $attribute['id'] = $this->getFormId(); $attribute['method'] = 'post'; $attribute['action'] = $this->getActionUrl(); $attribute['target'] = '_top'; $attribute['accept-charset'] = 'UTF-8'; $attribute[FE_INPUT_AUTOCOMPLETE] = 'on'; $attribute['enctype'] = $this->getEncType(); $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted 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 */ 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 \CodeException * @throws \DbException * @throws \UserFormException */ public function getEncType() { $result = $this->dbArray[$this->dbIndexQfq]->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(); /** * @param array|string $value * * @return array|string * @throws \CodeException * @throws \DbException * @throws \DownloadException * @throws \UserFormException * @throws \UserReportException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ private function processReportSyntax($value) { if (is_array($value)) { $new = array(); //might happen for e.g Template Groups foreach ($value as $item) { $new[] = $this->processReportSyntax($item); } return $new; } $value = trim($value); if (substr($value, 0, 8) == SHEBANG_REPORT) { if ($this->report === null) { $this->report = new Report(array(), $this->evaluate, false); } if ($this->bodytextParser === null) { $this->bodytextParser = new BodytextParser(); } $storeRecord = $this->store->getStore(STORE_RECORD); $value = $this->report->process($this->bodytextParser->process($value)); $this->store->setStore($storeRecord, STORE_RECORD, true); $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug } return $value; } /** * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON. * * @param int $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 \DownloadException * @throws \UserFormException * @throws \UserReportException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ public function elements($recordId, $filter, $feIdContainer, 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, FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON]; // get current data record $primaryKey = $this->formSpec[F_PRIMARY_KEY]; if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) { $tableName = $this->formSpec[F_TABLE_NAME]; $row = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM $tableName WHERE $primaryKey = ?", ROW_EXPECT_1, array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$primaryKey'='$recordId' from table '" . $this->formSpec[F_TABLE_NAME] . "'."); $this->store->setStore($row, STORE_RECORD); } $this->checkAutoFocus(); $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM); // 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(); // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM); // Fill STORE_LDAP $fe = $this->prepareFillStoreFireLdap($fe); if (isset($fe[FE_FILL_STORE_VAR])) { $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug $fe[FE_FILL_STORE_VAR] = $this->evaluate->parse($fe[FE_FILL_STORE_VAR], ROW_EXPECT_0_1); $this->store->appendToStore($fe[FE_FILL_STORE_VAR], STORE_VAR); } // for Upload FormElements, it's necessary to pre-calculate an optional given 'slaveId'. if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) { Support::setIfNotSet($fe, FE_SLAVE_ID); $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID])); $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR); } $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]); $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_NOTE, STORE_SYSTEM); // debug $fe[FE_NOTE] = $this->processReportSyntax($fe[FE_NOTE]); // ** evaluate current FormElement ** $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack); $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName); // Some Defaults $formElement = Support::setFeDefaults($formElement, $this->formSpec); // // Copy global readonly mode. // if ($this->formSpec[F_MODE] == F_MODE_READONLY) { // $fe[FE_MODE] = FE_MODE_READONLY; // $fe[FE_MODE_SQL] = ''; // } if ($flagOutput === true) { $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]); } $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]) : ''; } $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 } // In case the current element is a 'RETYPE' element: take the element name of the source FormElement. Needed to retrieve the default value later. $name = (isset($formElement[FE_RETYPE_SOURCE_NAME])) ? $formElement[FE_RETYPE_SOURCE_NAME] : $formElement[FE_NAME]; // Retrieve value via FSRVD $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE]; $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore); } if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) { // $value = htmlspecialchars_decode($value, ENT_QUOTES); $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value); } // 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, $formElement[FE_TG_INDEX]); // 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->showDebugInfoFlag) { 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("