* r= (table.id for a single record form) * keySemId,keySemIduser * */ /** * Class Qfq * @package qfq */ class QuickFormQuery { /** * @var Store instantiated class */ protected $store = null; /** * @var Database[] - Array of Database instantiated class */ protected $dbArray = array(); /** * @var Evaluate instantiated class */ protected $evaluate = null; protected $formSpec = array(); // Stores the form content after parsing SQL queries and QFQ syntax stored in form attributes protected $feSpecAction = array(); // Form Definition: copy of the loaded form protected $feSpecNative = array(); // FormEelement Definition: all formElement.class='action' of the loaded form protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form /** * @var array */ private $t3data = array(); // FormElement Definition: all formElement.class='native' of the loaded form /** * @var bool */ private $phpUnit = false; /** * @var bool */ private $inlineReport = false; /** * @var Session */ private $session = null; private $dbIndexData = false; private $dbIndexQfq = false; /* * TODO: * Preparation: setup logging, database access, record locking * fill stores * Check permission_create / permission_update * Multi: iterate over all records, Single: activate record * Check mode: Load | Save * doActions 'Before' * Do all FormElements * doActions 'After' */ /** * Construct the Form Class and Store too. This is the base initialization moment. * * As a result of instantiating of Form, the class Store will initially called the first time and therefore * instantiated automatically. Store might throw an exception, in case the URL-passed SIP is invalid. * * @param array $t3data * @param bool $phpUnit * @param bool $inlineReport * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ public function __construct(array $t3data = array(), $phpUnit = false, $inlineReport = true) { #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}" $this->phpUnit = $phpUnit; $this->inlineReport = $inlineReport; mb_internal_encoding("UTF-8"); $this->session = Session::getInstance($phpUnit); // Refresh the session even if no new data saved. Session::set(SESSION_LAST_ACTIVITY, time()); Support::setQfqErrorHandler(); // PHPExcel set_include_path(get_include_path() . PATH_SEPARATOR . '../../Resources/Private/Classes/'); // set dummy values if QuickFormQuery is not called by Typo3 $t3data[T3DATA_BODYTEXT] = $t3data[T3DATA_BODYTEXT] ?? ''; $t3data[T3DATA_UID] = $t3data[T3DATA_UID] ?? 0; $t3data[T3DATA_HEADER] = $t3data[T3DATA_HEADER] ?? ''; // Read report file, if file keyword exists in bodytext $reportPathFileNameFull = ReportAsFile::parseFileKeyword($t3data[T3DATA_BODYTEXT]); if ($reportPathFileNameFull !== null) { $t3data[T3DATA_BODYTEXT] = ReportAsFile::read_report_file($reportPathFileNameFull); } // SUPER HACK: to render inline editor when an exception is thrown // Can't use store, since store needs bodytext to be parsed, which might throw exceptions if there is a syntax error. \UserReportException::$report_uid = $t3data[T3DATA_UID]; \UserReportException::$report_bodytext = $t3data[T3DATA_BODYTEXT]; \UserReportException::$report_header = $t3data[T3DATA_HEADER]; \UserReportException::$report_pathFileName = $reportPathFileNameFull; $btp = new BodytextParser(); $t3data[T3DATA_BODYTEXT_RAW] = $t3data[T3DATA_BODYTEXT]; $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]); $this->t3data = $t3data; $bodytext = $this->t3data[T3DATA_BODYTEXT]; $this->store = Store::getInstance($bodytext, $phpUnit); $timeout = $this->store::getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM); Session::checkSessionExpired($timeout); // If an FE user logs out and a different user logs in (same browser session) - the old values has to be destroyed! if (Session::getAndDestroyFlagFeUserHasChanged()) { $this->store->unsetStore(STORE_USER); } $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3); $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM); $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM); $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData); if ($this->dbIndexData != $this->dbIndexQfq) { $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq); } $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]); $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM); $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq], $this->store); $updateDb->checkNupdate($dbUpdate); $this->store->FillStoreSystemBySql(); // Do this after the DB-update // Set dbIndex, evaluate any $dbIndex = $this->store->getVar(TOKEN_DB_INDEX, STORE_TYPO3 . STORE_EMPTY); $dbIndex = $this->evaluate->parse($dbIndex); $dbIndex = ($dbIndex == '') ? DB_INDEX_DEFAULT : $dbIndex; $this->store->setVar(TOKEN_DB_INDEX, $dbIndex, STORE_TYPO3); // Create report file if file keyword not found (and auto export is enabled in qfq settings) if ($reportPathFileNameFull === null && $t3data[T3DATA_UID] !== 0 && strtolower($this->store->getVar(SYSTEM_REPORT_AS_FILE_AUTO_EXPORT, STORE_SYSTEM)) === 'yes') { $reportPathFileNameFull = ReportAsFile::create_file_from_ttContent($t3data[T3DATA_UID], $this->dbArray[$this->dbIndexData]); } // Save pathFileName for use in inline editor $this->t3data[T3DATA_REPORT_PATH_FILENAME] = $reportPathFileNameFull; if (SYSTEM_REPORT_MIN_PHP_VERSION_YES == $this->store::getVar(SYSTEM_REPORT_MIN_PHP_VERSION, STORE_SYSTEM)) { if (version_compare(PHP_VERSION, MIN_PHP_VERSION) < 0) { throw new \UserReportException("Minimal required PHP Version: " . MIN_PHP_VERSION, ERROR_PHP_VERSION); } } } /** * Returns the defined forwardMode and set forwardPage * * @return array * @throws \CodeException * @throws \UserFormException */ public function getForwardMode() { if (!isset($this->formSpec[F_FORWARD_PAGE])) { // For QFQ inline editing: no redirect and no further processing. return [API_REDIRECT => API_ANSWER_REDIRECT_NO, API_REDIRECT_URL => '']; } $forwardPage = $this->formSpec[F_FORWARD_PAGE]; switch ($this->formSpec[F_FORWARD_MODE]) { case F_FORWARD_MODE_URL_SIP: $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL); $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL; break; case F_FORWARD_MODE_URL_SIP_SKIP_HISTORY: // F_FORWARD_MODE_URL_SIP is not defined in API PROTOCOL. At the moment it's only used for 'copyForm'. // 'copyForm' behaves better if the page is not in history. // An option for better implementing would be to separate SKIP History from ForwardMode. For API, it can be combined again. $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL); $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY; break; default: break; } return ([ API_REDIRECT => $this->formSpec[F_FORWARD_MODE], API_REDIRECT_URL => $forwardPage, ]); } /** * Main entry point to display content: a) form and/or b) report * * @return string * @throws \CodeException * @throws \DbException * @throws \DownloadException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception * @throws \Twig\Error\LoaderError * @throws \Twig\Error\RuntimeError * @throws \Twig\Error\SyntaxError * @throws \UserFormException * @throws \UserReportException */ public function process() { $html = ''; $render = $this->store->getVar(SYSTEM_RENDER, STORE_TYPO3 . STORE_SYSTEM); if ($render == SYSTEM_RENDER_API && isset($GLOBALS['TYPO3_CONF_VARS'])) { return ''; } if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') { $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0); $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']); } $html .= $this->doForm(FORM_LOAD); if ($render == SYSTEM_RENDER_BOTH || $render == SYSTEM_RENDER_API || ($render == SYSTEM_RENDER_SINGLE && $html == '')) { $html .= $this->doReport(); } // Only needed if there are potential 'download'-links, which shall show a popup during processing of the download. if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) { $html .= $this->getModalCode(); } // Only needed if there are 'drag and drop' elements. if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') { $html .= $this->getDragAndDropCode(); } $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM); if ($class) { $html = Support::wrapTag("
", $html); } return $html; } /** * Determine the name of the language parameter field, which has to be taken to fill language specific definitions. * * @throws \CodeException * @throws \UserFormException */ private function setParameterLanguageFieldName() { $typo3PageLanguage = $this->store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3); if (empty($typo3PageLanguage)) { return; } foreach (['A', 'B', 'C', 'D'] as $key) { $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id"; if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) { $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM); break; } } } /** * Creates an empty file. This indicates that the current form is in debug mode. Returns HTML element which will be * replaced by the logfile. * * @param $formName * @param $formLogMode * @return string * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ private function getFormLog($formName, $formLogMode) { $formLogFileName = Support::getFormLogFileName($formName, $formLogMode); file_put_contents($formLogFileName, ''); $monitor = new Monitor(); return "
Please wait
" . $monitor->process([TOKEN_L_FILE => $formLogFileName, TOKEN_L_APPEND => '1', TOKEN_L_HTML_ID => FORM_LOG_HTML_ID]); } /** * Process form. * $mode= * FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements * FORM_UPDATE: States and values of all form elements will be returned as JSON. * FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON. * FORM_DELETE: * * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE * * @return array|string * @throws \CodeException * @throws \DbException * @throws \DownloadException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception * @throws \Twig\Error\LoaderError * @throws \Twig\Error\RuntimeError * @throws \Twig\Error\SyntaxError * @throws \UserFormException * @throws \UserReportException */ private function doForm($formMode) { $data = ''; $foundInStore = ''; $flagApiStructureReGroup = true; $formModeNew = ''; $build = null; // Check if there is a recordId specified in Bodytext - parse if it is a query $rTmp = $this->store->getVar(SIP_RECORD_ID, STORE_TYPO3, SANITIZE_ALLOW_ALL); if (false !== $rTmp && !ctype_digit($rTmp)) { $rTmp = $this->evaluate->parse($rTmp); $this->store->setVar(SIP_RECORD_ID, $rTmp, STORE_TYPO3); } $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_TYPO3 . STORE_SIP . STORE_CLIENT . STORE_ZERO, SANITIZE_ALLOW_DIGIT, $foundInStore); $this->setParameterLanguageFieldName(); $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore, $formLogMode); if ($formName !== false && $formLogMode !== false) { return $this->getFormLog($formName, $formLogMode); } if ($formName === false) { switch ($formMode) { case FORM_DELETE: $formModeNew = FORM_DELETE; break; case FORM_DRAG_AND_DROP: throw new \CodeException('Missing form in SIP', ERROR_MISSING_FORM); default: return '';// No form found: do nothing } } // Check 'session expire' happens quite late, cause it can be configured per form. if ($formName !== false) { Session::checkSessionExpired($this->formSpec[F_SESSION_TIMEOUT_SECONDS]); } // Fill STORE_FORM: might need Form.fillStoreVar={{!SELECT ...}}) to provide STORE_VAR - therefore the FORM-definition should already been processed. #8058 switch ($formMode) { case FORM_UPDATE: case FORM_SAVE: case FORM_REST: $fillStoreForm = new FillStoreForm(); $fillStoreForm->process($formMode); // STORE_TYPO3 has been filled: fire fillStoreVar again. if (!empty($this->formSpec[FE_FILL_STORE_VAR])) { $this->fillStoreVar($this->formSpec[FE_FILL_STORE_VAR]); } break; } if ($formName !== false) { // Validate (only if there is a 'real' form, not a FORM_DELETE with only a table name). // Attention: $formModeNew will be set $sipFound = $this->validateForm($foundInStore, $formMode, $formModeNew); } else { // FORM_DELETE without a form definition: Fake the form with only a tableName. $table = $this->store->getVar(SIP_TABLE, STORE_SIP); if ($table === false) { throw new \UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE); } $sipFound = true; $this->formSpec[F_NAME] = ''; $this->formSpec[F_TABLE_NAME] = $table; $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS] = 1; // just indicate a timeout, the exact timeout is stored in the dirty record. $this->formSpec[F_DIRTY_MODE] = DIRTY_MODE_EXCLUSIVE; // just set a mode,, the exact mode is stored in the dirty record. $this->formSpec[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT; $tmpDbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP); if (!empty($tmpDbIndexData)) { $this->formSpec[F_DB_INDEX] = $tmpDbIndexData; if ($tmpDbIndexData != $this->dbIndexData) { if (!isset($this->dbArray[$tmpDbIndexData])) { $this->dbArray[$tmpDbIndexData] = new Database($tmpDbIndexData); } } } } // FormAsFile: Get the new and the old form file name and make sure both files are writable (before DB changes are made) // Note: This can't be done earlier because $formModeNew might be changed in the lines above. $formNameDB = FormAsFile::formNameFromFormRelatedRecord($recordId, $this->formSpec[F_TABLE_NAME] ?? '', $this->dbArray[$this->dbIndexQfq]); switch ($this->formSpec[F_TABLE_NAME] ?? '') { case TABLE_NAME_FORM: // cases covered: new form, existing form, existing form but form name changed $formFileName = $this->store->getVar(F_NAME, STORE_FORM, SANITIZE_ALLOW_ALNUMX); $formFileName = $formFileName === false ? $formNameDB : $formFileName; if ($formNameDB !== null && $formFileName !== $formNameDB && $formModeNew === FORM_SAVE) { $formFileNameDelete = $formNameDB; FormAsFile::enforceFormFileWritable($formFileNameDelete, $this->dbArray[$this->dbIndexQfq]); // file will be deleted after DB changes } break; case TABLE_NAME_FORM_ELEMENT: // cases covered: new formElement, existing formElement $formId = $this->store->getVar(FE_FORM_ID, STORE_FORM); $formFileName = $formId !== false ? FormAsFile::formNameFromFormRelatedRecord($formId, TABLE_NAME_FORM, $this->dbArray[$this->dbIndexQfq]) : $formNameDB; break; default: $formFileName = $formNameDB; } if ($formFileName !== null && in_array($formModeNew, [FORM_SAVE, FORM_DRAG_AND_DROP, FORM_DELETE])) { FormAsFile::enforceFormFileWritable($formFileName, $this->dbArray[$this->dbIndexQfq]); } // For 'new' record always create a new Browser TAB-uniq (for this current form, nowhere else used) SIP. // With such a Browser TAB-uniq SIP, multiple Browser TABs and following repeated NEWs are easily implemented. if ($formMode != FORM_REST) { if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) { $this->store->createSipAfterFormLoad($formName); } } // Fill STORE_BEFORE if ($formName !== false && $this->store->getVar($this->formSpec[F_PRIMARY_KEY], STORE_BEFORE) === false) { $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY], STORE_BEFORE); } // Check (and release) dirtyRecord. if ($formModeNew === FORM_DELETE || $formModeNew === FORM_SAVE) { $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq); $answer = $dirty->checkDirtyAndRelease($formModeNew, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS], $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $this->formSpec[F_PRIMARY_KEY], $recordId, true); // In case of a conflict, return immediately if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) { $answer[API_STATUS] = API_ANSWER_STATUS_ERROR; return $answer; } } // FORM_LOAD: if there is a foreign exclusive record lock - show form in F_MODE_READONLY mode. if ($formModeNew === FORM_LOAD) { $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq); $recordDirty = array(); $rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg); if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) { $this->formSpec[F_MODE_GLOBAL] = F_MODE_READONLY; } } switch ($formModeNew) { case FORM_DELETE: $build = new Delete($this->dbIndexData); break; case FORM_REST: break; case FORM_LOAD: case FORM_SAVE: case FORM_UPDATE: case FORM_DRAG_AND_DROP: $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]); $this->store->fillStoreTableDefaultColumnType($tableDefinition); // Check if the defined column primary key exist. if ($this->store::getVar($this->formSpec[F_PRIMARY_KEY], STORE_TABLE_COLUMN_TYPES) === false) { throw new \UserFormException("Primary Key '" . $this->formSpec[F_PRIMARY_KEY] . "' not found in table " . $this->formSpec[F_TABLE_NAME], ERROR_INVALID_OR_MISSING_PARAMETER); } switch ($this->formSpec['render']) { case 'plain': $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray); break; case 'table': $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray); break; case 'bootstrap': $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray); break; default: throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); } break; default: throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); } $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit); switch ($formModeNew) { case FORM_LOAD: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); // Build FORM $data = $build->process($formModeNew); $tmpClass = is_numeric($this->formSpec[F_BS_COLUMNS]) ? ('col-md-' . $this->formSpec[F_BS_COLUMNS]) : $this->formSpec[F_BS_COLUMNS]; // $data = Support::wrapTag("
formSpec[F_BS_COLUMNS] . "'>", $data); $data = Support::wrapTag('
', $data); $data = Support::wrapTag('
', $data); $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD); break; case FORM_UPDATE: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); // data['form-update']=.... $data = $build->process($formModeNew); $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD); break; case FORM_DELETE: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE); $build->process($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]); $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_DELETE); break; case FORM_SAVE: $this->logFormSubmitRequest(); $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3); // Action: Before $feTypeList = FE_TYPE_BEFORE_SAVE . ',' . ($recordId == 0 ? FE_TYPE_BEFORE_INSERT : FE_TYPE_BEFORE_UPDATE); $formAction->elements($recordId, $this->feSpecAction, $feTypeList); // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten. $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]); $this->ifPillIsHiddenSetChildFeToHidden(); // SAVE $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw); $save->processAllImageCutFE(); $save->checkRequiredHidden(); $rc = $save->process(); $save->processAllUploads($rc); // Action: After*, Sendmail $feTypeList = FE_TYPE_SENDMAIL . ',' . FE_TYPE_AFTER_SAVE . ',' . ($recordId == 0 ? FE_TYPE_AFTER_INSERT : FE_TYPE_AFTER_UPDATE); $status = $formAction->elements($rc, $this->feSpecAction, $feTypeList); if ($status != ACTION_ELEMENT_NO_CHANGE) { // Reload fresh saved record and fill STORE_RECORD with it. $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]); } // Action: Paste $this->pasteClipboard($this->formSpec[F_ID], $formAction); if ($formMode == FORM_REST) { $data = $this->doRestPostPut($rc); $flagApiStructureReGroup = false; break; } $this->setForwardModePage(); // Logic: If a) r=0 and // b) final: (forwardMode=='auto' and User presses only 'save' (not 'save & close')) OR (forwardMode=='no') // then the client should reload the current page with the newly created record. A new SIP is necessary! $getJson = true; if (0 == $this->store->getVar(SIP_RECORD_ID, STORE_SIP) && (($this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_AUTO && API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX) ) || $this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_NO)) { $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc); $getJson = false; } if ($getJson) { // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups. $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec); $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM); $feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName); $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($feSpecNative, $this->formSpec[F_ID], $recordId); $data = $build->process($formModeNew, false, $this->feSpecNative); } break; case FORM_DRAG_AND_DROP: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); $dragAndDrop = new DragAndDrop($this->formSpec); $data = $dragAndDrop->process(); $flagApiStructureReGroup = false; $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD); break; case FORM_REST: $flagApiStructureReGroup = false; $data = $this->doRestGet(); break; default: throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); } if ($flagApiStructureReGroup && is_array($data)) { // $data['element-update']=... $data = $this->groupElementUpdateEntries($data); } // export Form to file, if loaded record is a Form/FormElement if ($formFileName !== null) { switch ($formModeNew) { case FORM_SAVE: case FORM_DRAG_AND_DROP: FormAsFile::exportForm($formFileName, $this->dbArray[$this->dbIndexQfq]); break; case FORM_DELETE: if (TABLE_NAME_FORM_ELEMENT === ($this->formSpec[F_TABLE_NAME] ?? '')) { FormAsFile::exportForm($formFileName, $this->dbArray[$this->dbIndexQfq]); } else { FormAsFile::deleteFormFile($formFileName, $this->dbArray[$this->dbIndexQfq], 'Form was deleted using form-editor.'); } break; } } // delete old form file if form name was changed if (isset($formFileNameDelete)) { FormAsFile::deleteFormFile($formFileNameDelete, $this->dbArray[$this->dbIndexQfq], "Form was renamed to: '$formFileName'."); } return $data; } /** * @return array * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function doRestGet() { $this->nameGenericRestParam(); $r = $this->store::getVar(TYPO3_RECORD_ID, STORE_TYPO3); $key = empty($r) ? F_REST_SQL_LIST : F_REST_SQL_DATA; if (!isset($this->formSpec[$key])) { throw new \UserFormException("Missing Parameter '$key'", ERROR_INVALID_VALUE); } return $this->evaluate->parse($this->formSpec[$key]); } /** * @return bool|array * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function doRestPostPut($id) { if (!isset($this->formSpec[F_REST_SQL_POST_PUT])) { return ['id' => $id]; } $this->nameGenericRestParam(); return $this->evaluate->parse($this->formSpec[F_REST_SQL_POST_PUT]); } /** * Checks if $serverToken matches HTTP_HEADER_AUTHORIZATION, * If not: throw an exception. * * @param string|array $serverToken * @throws \CodeException * @throws \UserFormException */ private function restCheckAuthToken($serverToken) { // No serverToken: no check necessary if ($serverToken === '') { return; } $clientToken = $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT, SANITIZE_ALLOW_ALL); if ($serverToken === $clientToken) { return; } // Delay before answering. $seconds = $this->store::getVar(SYSTEM_SECURITY_FAILED_AUTH_DELAY, STORE_SYSTEM); sleep($seconds); if ($clientToken == false) { throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Missing authorization token', ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION, ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED ]), ERROR_REST_AUTHORIZATION); } throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Authorization token not accepted', ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION, ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED ]), ERROR_REST_AUTHORIZATION); } /** * STORE_CLIENT: copy parameter _id1,_id2,...,_idN to named variables, specified via $this->formSpec[F_REST_PARAM] (CSV list) * * @throws \CodeException * @throws \UserFormException */ private function nameGenericRestParam() { $paramNames = explode(',', $this->formSpec[F_REST_PARAM] ?? ''); $ii = 1; foreach ($paramNames as $key) { switch ($key) { case CLIENT_FORM: case CLIENT_RECORD_ID: throw new \UserFormException("Name '$key' is forbidden in " . F_REST_PARAM, ERROR_INVALID_VALUE); break; default: break; } $val = $this->store::getVar(CLIENT_REST_ID . $ii, STORE_CLIENT); $this->store::setVar($key, $val, STORE_CLIENT); $ii++; } } /** * Copies state 'hidden' from a FE pill to all FE child elements of that pill. * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function ifPillIsHiddenSetChildFeToHidden() { $feFilter = OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL); if (!empty($feFilter)) { foreach ($feFilter as $feParent) { if ($feParent[FE_MODE_SQL]) { $mode = $this->evaluate->parse($feParent[FE_MODE_SQL]); if ($mode != '') { $feParent[FE_MODE] = $mode; } } if ($feParent[FE_MODE] == FE_MODE_HIDDEN) { $feChild = OnArray::filter($this->feSpecNative, FE_ID_CONTAINER, $feParent[FE_ID]); foreach ($feChild as $fe) { # Search for origin foreach ($this->feSpecNative as $key => $value) { if ($value[FE_ID] == $fe[FE_ID]) { $this->feSpecNative[$key][FE_MODE] = FE_MODE_HIDDEN; break; } } } } } } } /** * @throws \CodeException * @throws \DbException * @throws \UserFormException */ private function logFormSubmitRequest() { $formSubmitLogMode = $this->formSpec[F_FORM_SUBMIT_LOG_MODE] ?? $this->store->getVar(SYSTEM_FORM_SUBMIT_LOG_MODE, STORE_SYSTEM, SANITIZE_ALLOW_ALNUMX); if ($formSubmitLogMode === FORM_SUBMIT_LOG_MODE_NONE) { return; } $formData = $_POST; unset($formData[CLIENT_SIP]); $formData = json_encode($formData, JSON_UNESCAPED_UNICODE); $clientIp = $_SERVER[CLIENT_REMOTE_ADDRESS] ?? ''; $userAgent = $_SERVER[CLIENT_HTTP_USER_AGENT] ?? ''; $sipData = json_encode($this->store->getStore(STORE_SIP), JSON_UNESCAPED_UNICODE); $formId = $this->formSpec[F_ID]; $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); $feUser = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX); $pageId = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX); $sessionId = session_id(); $sql = "INSERT INTO `FormSubmitLog` (`formData`, `sipData`, `clientIp`, `feUser`, `userAgent`, `formId`, `recordId`, `pageId`, `sessionId`, `created`)" . "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"; $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $recordId, $pageId, $sessionId]; $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params); } /** * Check if forwardMode='url...'. * yes: process 'forwardPage' and fill $this->formSpec[F_FORWARD_MODE] and $this->formSpec[F_FORWARD_PAGE] * no: do nothing * * '$this->formSpec[F_FORWARD_PAGE]' might give a new forwardMode. If so, set $this->formSpec[F_FORWARD_MODE] to * it. * * '$this->formSpec[F_FORWARD_PAGE]': * a) url http://www.nzz.ch/index.html?a=123#bottom, website.html?a=123#bottom, * ?[id=]&a=123#bottom, ?id=&a=123#bottom * b) mode no|client|url|... * c) mode|url combination of above * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function setForwardModePage() { if (F_FORWARD_MODE_URL != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) { return; } $forwardPageTmp = $this->evaluate->parse($this->formSpec[F_FORWARD_PAGE]); // Format: [mode/url][|url] $forwardArray = explode('|', $forwardPageTmp, 2); $forward = trim($forwardArray[0]); switch ($forward) { case F_FORWARD_MODE_AUTO: case F_FORWARD_MODE_CLOSE: case F_FORWARD_MODE_NO: case F_FORWARD_MODE_URL: case F_FORWARD_MODE_URL_SKIP_HISTORY: case F_FORWARD_MODE_URL_SIP: case F_FORWARD_MODE_URL_SIP_SKIP_HISTORY: $this->formSpec[F_FORWARD_MODE] = $forward; if (isset($forwardArray[1])) { $this->formSpec[F_FORWARD_PAGE] = trim($forwardArray[1]); } else { $this->formSpec[F_FORWARD_PAGE] = ''; } break; default: $this->formSpec[F_FORWARD_PAGE] = $forward; break; } if (F_FORWARD_MODE_URL == substr($this->formSpec[F_FORWARD_MODE], 0, 3)) { if ($this->formSpec[F_FORWARD_PAGE] == '') { $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO; } } } /** * Iterate over all Clipboard source records and fire for each all FE.type=paste records. * * @param int $formId * @param FormAction $formAction * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function pasteClipboard($formId, FormAction $formAction) { if (!$this->isPasteRecord()) { return; } $cookieQfq = $this->store->getVar(CLIENT_COOKIE_QFQ, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX); if ($cookieQfq === false || $cookieQfq == '') { throw new \UserFormException('Qfq Session missing', ERROR_QFQ_SESSION_MISSING); } # select clipboard records $sql = "SELECT c.idSrc as id, c.xId FROM `Clipboard` AS c WHERE `c`.`cookie`='$cookieQfq' AND `c`.`formIdPaste`=$formId ORDER BY `c`.`id`"; $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql); // Process clipboard records. foreach ($arrClipboard as $srcIdRecord) { $formAction->doAllFormElementPaste($this->feSpecAction, $this->formSpec[F_TABLE_NAME], $this->formSpec[F_TABLE_NAME], "", $srcIdRecord); } } # doClipboard() /** * @return bool true if there is at least one paste record, else false. */ private function isPasteRecord() { foreach ($this->feSpecAction as $formElement) { if ($formElement[FE_TYPE] == FE_TYPE_PASTE) { return true; } } return false; } /** * Set F_FORWARD_MODE to F_FORWARD_MODE_PAGE and builds a redirection URL to the current page with the already * used parameters. Do this by building a new SIP with the new recordId. * * @param array $formSpec * @param int $recordId * * @return array * @throws \CodeException * @throws \UserFormException */ private function buildNSetReloadUrl(array $formSpec, $recordId) { $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY; // Rebuild original URL $storeT3 = $this->store->getStore(STORE_TYPO3); $storeT3['id'] = $storeT3[TYPO3_PAGE_ID]; $storeT3 = OnArray::getArrayItems($storeT3, ['id', TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE], true, true); $arr = KeyValueStringParser::parse($this->store->getVar(SIP_URLPARAM, STORE_SIP), '=', '&'); $arr[SIP_RECORD_ID] = $recordId; $arr = array_merge($storeT3, $arr); $queryString = KeyValueStringParser::unparse($arr, '=', '&'); $formSpec[F_FORWARD_PAGE] = store::getSipInstance()->queryStringToSip($queryString, RETURN_URL); return $formSpec; } /** * Checks if there is formLog mode active for FORM_LOG_SESSION or FORM_LOG_ALL. * If yes, set $form[FORM_LOG_FILE_SESSION] resp. $form[FORM_LOG_FILE_ALL]. * If the last action is older FORM_LOG_FILE_EXPIRE, the file will be deleted and formLog mode stops (disabled). * * @param array $form * @return array * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ private function checkFormLogMode(array $form) { $form[FORM_LOG_FILE_SESSION] = ''; $form[FORM_LOG_FILE_ALL] = ''; foreach ([FORM_LOG_SESSION, FORM_LOG_ALL] as $mode) { $file = Support::getFormLogFileName($form[F_NAME], $mode); if (file_exists($file) && false !== ($arr = stat($file))) { if (time() - $arr['mtime'] > FORM_LOG_FILE_EXPIRE) { HelperFile::unlink($file); } else { $form[FORM_LOG_FILE . '_' . $mode] = $file; $form[FORM_LOG_ACTIVE] = 1; } } } return $form; } /** * Get form name * Check if the form is in log mode: set formLog and return * Load form. Evaluates form. Load FormElements. * * After processing: * Loaded Form is in $this->formSpec * Loaded 'action' FormElements are in $this->feSpecAction * Loaded 'native' FormElements are in $this->feSpecNative * * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE|FORM_REST * @param int $recordId * @param string $foundInStore * @param string $formLogMode * @return bool|string if found the formName, else 'false'. * * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function loadFormSpecification($mode, $recordId, &$foundInStore = '', &$formLogMode = '') { $formLogMode = false; // Important: if no form is found, formLogMode needs also to be false. // formName if (false === ($formName = $this->getFormName($mode, $foundInStore))) { return false; } // Check for '_formLogMode'=logSession|logAll $formLogMode = $this->store::getVar(FORM_LOG_MODE, STORE_SIP); if ($formLogMode !== false) { return $formName; // fomLog: getting the formName is sufficient. } if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) { throw new \UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE); } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM); // Check for form file changes FormAsFile::importForm($formName, $this->dbArray[$this->dbIndexQfq]); // Load form $constant = F_NAME; // PhpStorm complains if the constant is directly defined in the string below $form = $this->dbArray[$this->dbIndexQfq]->sql("SELECT * FROM `Form` AS f WHERE `f`.`$constant` LIKE ? AND `f`.`deleted`='no'", ROW_EXPECT_1, [$formName], 'Form "' . $formName . '" not found or multiple forms with the same name.'); // Import Form from file if loaded record is Form/FormElement (If form file was changed, throw exception) FormAsFile::importFormRecordId($recordId, $form[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]); $form = $this->checkFormLogMode($form); $form = $this->modeCleanFormConfig($mode, $form); HelperFormElement::explodeParameter($form, F_PARAMETER); unset($form[F_PARAMETER]); // Save specific elements to be expanded later. $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE, FE_FILL_STORE_VAR, F_REST_SQL_LIST, F_REST_SQL_DATA, F_REST_SQL_POST_PUT, F_MULTI_SQL]); $form[FE_FILL_STORE_VAR] = ''; $form[F_FORWARD_PAGE] = ''; $form[F_REST_SQL_LIST] = ''; $form[F_REST_SQL_DATA] = ''; // Setting defaults later is too late. if (empty($form[F_DB_INDEX])) { $form[F_DB_INDEX] = $this->dbIndexData; } else { $form[F_DB_INDEX] = $this->evaluate->parse($form[F_DB_INDEX]); } if (empty($form[F_PRIMARY_KEY])) { $form[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT; } // Some forms load/save the form data on extra defined databases. if ($this->dbIndexData != $form[F_DB_INDEX]) { if (!isset($this->dbArray[$form[F_DB_INDEX]])) { $this->dbArray[$form[F_DB_INDEX]] = new Database($form[F_DB_INDEX]); } $this->dbIndexData = $form[F_DB_INDEX]; unset($this->evaluate); $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]); } // This is needed for filling templateGroup records with their default values // and for evaluating variables in the Form title $this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]); // In case $form[F_REST_TOKEN] is a query which results to an empty answer; every token will fail. $flagRestToken = !empty($form[F_REST_TOKEN]); // Evaluate all fields $formSpec = $this->evaluate->parseArray($form); // If it is empty, set it to true to force the TOKEN check (which will always fail) if ($flagRestToken && $form[F_REST_TOKEN] == '') { $form[F_REST_TOKEN] = true; } $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM); $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName); if (!empty($formSpec[F_SUBMIT_BUTTON_TEXT])) { // set defaults for submit button (different from save button defaults) $formSpec[F_SUBMIT_BUTTON_CLASS] = $formSpec[F_SAVE_BUTTON_CLASS] ?? 'btn btn-default'; $formSpec[F_SUBMIT_BUTTON_GLYPH_ICON] = $formSpec[F_SAVE_BUTTON_GLYPH_ICON] ?? ''; $formSpec[F_SUBMIT_BUTTON_TOOLTIP] = $formSpec[F_SAVE_BUTTON_TOOLTIP] ?? $formSpec[F_SUBMIT_BUTTON_TEXT]; } $formSpec = $this->syncSystemFormConfig($formSpec); // Set form parameter which are expected to exist. $formSpec = $this->initForm($formSpec, $recordId); $formSpec = array_merge($formSpec, $parseLater); // Set F_FINAL_DELETE_FORM $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME]; // LOG !empty($form[FORM_LOG_ACTIVE]) && Logger::logFormLine($form, "F:$mode:evaluated:" . date('Y-m-d H:i:s'), $form, true); // Fire FE_FILL_STORE_VAR after the primary form record has been loaded if (!empty($formSpec[FE_FILL_STORE_VAR])) { $this->fillStoreVar($formSpec[FE_FILL_STORE_VAR]); // unset($formSpec[FE_FILL_STORE_VAR]); } if ($formSpec[F_FORWARD_MODE] === '') { // This should not happen since '' is not a valid choice for column forwardMode. But it happened anyway. So to be safe. $formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO; } $this->formSpec = $formSpec; // Clear $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM); // Read all 'active' FE $this->feSpecNativeRaw = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_RAW, ROW_REGULAR, [$this->formSpec["id"]]); // FE: Action $this->feSpecAction = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']); HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER); // FE: Native & Container // "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.feIdContainer = ? AND fe.enabled='yes' ORDER BY fe.ord, fe.id"; $feSpecNative = array(); switch ($mode) { case FORM_LOAD: // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level. $feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec); break; case FORM_SAVE: case FORM_UPDATE: case FORM_REST: $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec); break; case FORM_DELETE: $this->feSpecNative = array(); break; default: break; } $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName); $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId); return $formName; } /** * If $sql selects one row, append the row to STORE_VAR. * * @param $sql * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function fillStoreVar($sql) { if (empty($sql)) { return; } $rows = $this->evaluate->parse($sql, ROW_EXPECT_0_1); if (is_array($rows)) { $this->store->appendToStore($rows, STORE_VAR); // LOG if (!empty($form[FORM_LOG_ACTIVE])) { Logger::logFormLine($form, "F:add to STORE_VAR", $rows); } } else { if (!empty($rows)) { throw new \UserFormException("Invalid statement for '" . FE_FILL_STORE_VAR . "': " . $this->formSpec[FE_FILL_STORE_VAR], ERROR_INVALID_OR_MISSING_PARAMETER); } } } /** * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements. * This code is dirty: the nearly same function exists in class 'Database' - the difference is only * 'explodeTemplateGroupElements()'. * * @param string $sql SQL_FORM_ELEMENT_SPECIFIC_CONTAINER | SQL_FORM_ELEMENT_ALL_CONTAINER * @param array $param Parameter which matches the prepared statement in $sql * @param array $formSpec Main FormSpec to copy generic parameter to FormElements * * @return array|int * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ public function getNativeFormElements($sql, array $param, $formSpec) { $feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $param); $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative, $formSpec); // Explode and Do $FormElement.parameter HelperFormElement::explodeParameterInArrayElements($feSpecNative, FE_PARAMETER); // Check for retype FormElements which have to duplicated. $feSpecNative = HelperFormElement::duplicateRetypeElements($feSpecNative); // Check for templateGroup Elements to explode them $feSpecNative = $this->explodeTemplateGroupElements($feSpecNative); // Copy Attributes to FormElements $feSpecNative = HelperFormElement::copyAttributesToFormElements($formSpec, $feSpecNative); return $feSpecNative; } /** * Iterate over all FormElements in $elements. If a row has a column NAME_TG_COPIES, copy those elements * NAME_TG_COPIES-times. Adjust FE_TEMPLATE_GROUP_NAME_PATTERN (='%d') with current count on column FE_NAME and * FE_LABEL. * * This code is dirty: only to get JSON value, we have to initialize the STORE_RECORD (done earlier) to be capable * to parse fe[FE_VALUE], which probably contains as string like '{{!SELECT value FROM table WHERE xId={{id}} ORDER * BY id}}' - the {{id}} needs to be replaced by the current recordId (primary record). * * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad! * * @param array $elements * * @return array * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ private function explodeTemplateGroupElements(array $elements) { $new = array(); // No FormElements or no NAME_TG_COPIES column: nothing to do, return. if ($elements == array() || count($elements) == 0 || !isset($elements[0][NAME_TG_COPIES])) { return $elements; } // Iterate over all foreach ($elements as $row) { if (isset($row[NAME_TG_COPIES]) && $row[NAME_TG_COPIES] > 0) { $row[FE_VALUE] = $this->evaluate->parse($row[FE_VALUE]); for ($ii = 1; $ii <= $row[NAME_TG_COPIES]; $ii++) { $tmpRow = $row; if (is_array($row[FE_VALUE])) { $tmpRow[FE_VALUE] = ($ii <= count($row[FE_VALUE])) ? current($row[FE_VALUE][$ii - 1]) : ''; } unset($tmpRow[NAME_TG_COPIES]); $tmpRow[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $tmpRow[FE_NAME]); $tmpRow[FE_LABEL] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $tmpRow[FE_LABEL]); $tmpRow[FE_TG_INDEX] = $ii; $new[] = $tmpRow; } } else { $new[] = $row; } } return $new; } /** * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL). * * FORM_LOAD: * Specified in T3 body text with form= Returned Store:Typo3 * Specified in T3 body text with form={{form}} ':FSRD' Returned Store:SIP * Specified in T3 body text with form={{form:C:ALNUMX}} Returned Store:Client * Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE id={{conferenceId:S0}} * }} Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE * id={{conferenceId:C0:DIGIT}} }} Specified in SIP * * FORM_SAVE: * Specified in SIP * * * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE|FORM_REST * @param string $foundInStore * * @return bool|string Formname (Form.name) or FALSE (if no formname found) * @throws \CodeException * @throws \DbException * @throws \UserFormException * @throws \UserReportException */ public function getFormName($mode, &$foundInStore = '') { $dummy = array(); switch ($mode) { case FORM_LOAD: case FORM_REST: $store = STORE_TYPO3; break; case FORM_SAVE: case FORM_UPDATE: case FORM_DELETE: case FORM_DRAG_AND_DROP: $store = STORE_SIP; break; default: throw new \CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE); } $storeFormName = $this->store->getVar(SIP_FORM, $store, '', $foundInStore); $formName = $this->evaluate->parse($storeFormName, ROW_IMPLODE_ALL, 0, $dummy, $foundInStore); // If the formname is empty or if 'form' has not been found in any store: no form. if ($formName === '' || $foundInStore === '') { return false; } return $formName; } /** * Depending on $mode various formSpec fields might be adjusted. * E.g.: the form title is not important during a delete. * * @param string $mode * @param array $form * * @return array * @throws \CodeException * @throws \UserFormException */ private function modeCleanFormConfig($mode, array $form) { switch ($mode) { case FORM_DELETE: $form[F_TITLE] = ''; break; default: break; } unset($form[F_NOTE_INTERNAL]); if (isset($form[F_ESCAPE_TYPE_DEFAULT]) && $form[F_ESCAPE_TYPE_DEFAULT] == TOKEN_ESCAPE_CONFIG) { $form[F_ESCAPE_TYPE_DEFAULT] = $this->store->getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM); } return $form; } /** * The named $keys will be synced between STORE_SYSTEM and $formSpec (both directions). * The per form definition has precedence over STORE_SYSTEM. * STORE_SYSTEM if filled with the default values (config.qfq.php or if not exist than QFQ hardcoded) * Copying the 'Form' definition back to the system store helps to access the values * by '{{ ...:Y}}' (system store). E.g. the value of bs-*-columns might be displayed as placeholder in the * corresponding input field. * * @param array $formSpec * * @return array * @throws \CodeException * @throws \UserFormException */ private function syncSystemFormConfig(array $formSpec) { $keys = [F_BS_COLUMNS, F_BS_LABEL_COLUMNS, F_BS_INPUT_COLUMNS, F_BS_NOTE_COLUMNS, F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR, F_CLASS, F_CLASS_PILL, F_CLASS_BODY, F_BUTTON_ON_CHANGE_CLASS, F_ESCAPE_TYPE_DEFAULT, F_SAVE_BUTTON_TEXT, F_SAVE_BUTTON_TOOLTIP, F_SAVE_BUTTON_CLASS, F_SAVE_BUTTON_GLYPH_ICON, F_CLOSE_BUTTON_TEXT, F_CLOSE_BUTTON_TOOLTIP, F_CLOSE_BUTTON_CLASS, F_CLOSE_BUTTON_GLYPH_ICON, F_DELETE_BUTTON_TEXT, F_DELETE_BUTTON_TOOLTIP, F_DELETE_BUTTON_CLASS, F_DELETE_BUTTON_GLYPH_ICON, F_NEW_BUTTON_TEXT, F_NEW_BUTTON_TOOLTIP, F_NEW_BUTTON_CLASS, F_NEW_BUTTON_GLYPH_ICON, F_RECORD_LOCK_TIMEOUT_SECONDS, FE_INPUT_EXTRA_BUTTON_INFO_CLASS, F_SHOW_ID_IN_FORM_TITLE, F_INPUT_CLEAR_ME, FE_FILE_MAX_FILE_SIZE, F_FE_DATA_PATTERN_ERROR_SYSTEM, // Not a classical element to overwrite by form definition, but should be copied to detect changes per custom setting. ]; // By definition: existing vars which are empty, means: EMPTY - do not use any default! // But: a) if these variables are table columns, they always exist. For those: empty value means 'not set' // b) some values have a special meaning. E.g. empty FE_FILE_MAX_FILE_SIZE means take system config // - unset those. foreach ([F_BS_LABEL_COLUMNS, F_BS_INPUT_COLUMNS, F_BS_NOTE_COLUMNS, F_ESCAPE_TYPE_DEFAULT, FE_FILE_MAX_FILE_SIZE] as $key) { if (($formSpec[$key] ?? '') == '') { unset ($formSpec[$key]); } } if ($formSpec[F_FE_LABEL_ALIGN] == F_FE_LABEL_ALIGN_DEFAULT) { $formSpec[F_FE_LABEL_ALIGN] = $this->store->getVar(SYSTEM_LABEL_ALIGN, STORE_SYSTEM . STORE_EMPTY); } $storeSystem = $this->store::getStore(STORE_SYSTEM); foreach ($keys as $key) { if (isset($formSpec[$key])) { $this->store->setVar($key, $formSpec[$key], STORE_SYSTEM); } else { // if not found set '' $formSpec[$key] = $storeSystem[$key] ?? ''; } } return $formSpec; } /** * Set form parameter which are expected to exist. * * @param array $formSpec * @param int $recordId * * @return array * @throws \CodeException * @throws \UserFormException * @throws \UserReportException */ private function initForm(array $formSpec, $recordId) { Support::setIfNotSet($formSpec, F_EXTRA_DELETE_FORM, ''); Support::setIfNotSet($formSpec, F_SUBMIT_BUTTON_TEXT, ''); Support::setIfNotSet($formSpec, F_BUTTON_ON_CHANGE_CLASS, ''); Support::setIfNotSet($formSpec, F_LDAP_USE_BIND_CREDENTIALS, ''); Support::setIfNotSet($formSpec, F_DB_INDEX, $this->store->getVar(F_DB_INDEX, STORE_SYSTEM)); Support::setIfNotSet($formSpec, F_ENTER_AS_SUBMIT, $this->store->getVar(SYSTEM_ENTER_AS_SUBMIT, STORE_SYSTEM)); Support::setIfNotSet($formSpec, F_SESSION_TIMEOUT_SECONDS, $this->store->getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM)); Support::setIfNotSet($formSpec, F_FE_REQUIRED_POSITION, F_FE_REQUIRED_POSITION_LABEL_RIGHT); Support::setIfNotSet($formSpec, F_MULTI_MSG_NO_RECORD, F_MULTI_MSG_NO_RECORD_TEXT); Support::setIfNotSet($formSpec, F_FE_MIN_WIDTH, F_FE_MIN_WIDTH_DEFAULT); Support::setIfNotSet($formSpec, FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH, FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH_DEFAULT); Support::setIfNotSet($formSpec, F_ACTIVATE_FIRST_REQUIRED_TAB, 1); // In case there is no F_MODE defined on the form, check if there is one in STORE_SIP. // if ($formSpec[F_MODE] == '') { // $formModeGlobal = $this->store->getVar(F_MODE_GLOBAL, STORE_SIP); // if ($formModeGlobal !== false) { // $formSpec[F_MODE] = $formModeGlobal; // } // } // // Check for deprecated legacy code if (isset($formSpec['mode'])) { throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Outdated form definition', ERROR_MESSAGE_TO_DEVELOPER => "form.parameter.mode is deprecated. Please use form.parameter.formModeGlobal instead." ])); } // Unify F_MODE_GLOBAL $formSpec[F_MODE_GLOBAL] = Support::getFormModeGlobal($formSpec[F_MODE_GLOBAL] ?? ''); if ($formSpec[F_MODE_GLOBAL] == F_MODE_READONLY) { $formSpec[F_SHOW_BUTTON] = FORM_BUTTON_CLOSE; $formSpec[F_SUBMIT_BUTTON_TEXT] = ''; } if ($formSpec[F_ESCAPE_TYPE_DEFAULT] == TOKEN_ESCAPE_CONFIG) { $formSpec[F_ESCAPE_TYPE_DEFAULT] = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM); } // Append recordId to title if ($formSpec[F_SHOW_ID_IN_FORM_TITLE] == '1') { $formSpec[F_TITLE] .= ($recordId == 0) ? " (new)" : " ($recordId)"; } return $formSpec; } /** * Check if the form loading is permitted. If not, throw an exception. * * @param string $formNameFoundInStore * @param string $formMode * * @param $formModeNew * @return bool 'true' if SIP exists, else 'false' * @throws \CodeException * @throws \UserFormException */ private function validateForm($formNameFoundInStore, $formMode, &$formModeNew) { $formModeNew = $formMode; // Retrieve record_id either from SIP (preferred) or via URL $r = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT, '', $recordIdFoundInStore); // No record id: Fake a definition in STORE_TYPO3. if ($r === false) { $r = 0; $this->store->setVar(TYPO3_RECORD_ID, $r, STORE_TYPO3); $recordIdFoundInStore = STORE_TYPO3; } // If there is a record_id>0: EDIT else NEW: 'sip','logged_in','logged_out','always','never' $permitMode = ($r > 0) ? $this->formSpec['permitEdit'] : $this->formSpec['permitNew']; $feUserLoggedIn = isset($GLOBALS["TSFE"]->fe_user->user["uid"]) && $GLOBALS["TSFE"]->fe_user->user["uid"] > 0; $sipFound = $this->store->getVar(SIP_SIP, STORE_SIP) !== false; if ($sipFound) { if (($formNameFoundInStore === STORE_CLIENT) || ($recordIdFoundInStore === STORE_CLIENT)) { throw new \UserFormException("SIP exist but FORM or RECORD_ID are given by CLIENT.", ERROR_SIP_EXIST_BUT_OTHER_PARAM_GIVEN_BY_CLIENT); } } if ($formMode == FORM_REST) { $method = $this->store::getVar(CLIENT_REQUEST_METHOD, STORE_CLIENT); if (false === Support::findInSet(strtolower($method), $this->formSpec[F_REST_METHOD])) { throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Invalid HTTP method: endpoint (form) not found or access not allowed', ERROR_MESSAGE_TO_DEVELOPER => "Endpoint '" . $this->formSpec[F_NAME] . "' is not allowed with HTTP method '$method'", ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED ]), ERROR_FORM_REST); } $this->restCheckAuthToken($this->formSpec[F_REST_TOKEN] ?? ''); switch ($method) { case REQUEST_METHOD_GET: break; case REQUEST_METHOD_POST: case REQUEST_METHOD_PUT: $formModeNew = FORM_SAVE; break; case REQUEST_METHOD_DELETE: $formModeNew = FORM_DELETE; break; default: throw new \CodeException('This code should never be reached', ERROR_CODE_SHOULD_NOT_HAPPEN); } } else { switch ($permitMode) { case FORM_PERMISSION_SIP: if (!$sipFound || $formNameFoundInStore !== STORE_SIP || $recordIdFoundInStore !== STORE_SIP) { throw new \UserFormException("SIP Parameter needed for this form.", ERROR_SIP_NEEDED_FOR_THIS_FORM); } break; case FORM_PERMISSION_LOGGED_IN: if (!$feUserLoggedIn) { throw new \UserFormException("User not logged in.", ERROR_USER_NOT_LOGGED_IN); } break; case FORM_PERMISSION_LOGGED_OUT: if ($feUserLoggedIn) { throw new \UserFormException("User logged in.", ERROR_USER_LOGGED_IN); } break; case FORM_PERMISSION_ALWAYS: break; case FORM_PERMISSION_NEVER: throw new \UserFormException("Loading form forbidden.", ERROR_FORM_FORBIDDEN); break; default: throw new \CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE); } } if ($formMode === FORM_DELETE) { return $sipFound; } $sipArray = $this->store->getStore(STORE_SIP); // Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId' $requiredParameter = ($r > 0) ? $this->formSpec[F_REQUIRED_PARAMETER_EDIT] : $this->formSpec[F_REQUIRED_PARAMETER_NEW]; if (trim($requiredParameter) == '') { return $sipFound; } $requiredParameterArr = explode('#', $requiredParameter, 2); $param = explode(',', $requiredParameterArr[0]); foreach ($param as $name) { $name = trim($name); if ($name === '') { continue; } if (!isset($sipArray[$name])) { throw new \UserFormException("Missing required SIP parameter: $name", ERROR_MISSING_REQUIRED_PARAMETER); } } return $sipFound; } /** * Searches the whole array $dataArray on the second level for API_ELEMENT_UPDATE. * All found elements collect under $collect[API_ELEMENT_UPDATE]... . Leave the rest unchanged. * * @param array $dataArray * * @return array to build JSON */ private function groupElementUpdateEntries(array $dataArray) { $collect = array(); foreach ($dataArray as $data) { if (isset($data[API_ELEMENT_UPDATE])) { foreach ($data[API_ELEMENT_UPDATE] as $key => $item) { $collect[API_ELEMENT_UPDATE][$key] = $item; } unset($data[API_ELEMENT_UPDATE]); } if (is_array($data) && count($data) > 0) { $collect[API_FORM_UPDATE][] = $data; } } return $collect; } /** * Process the SQL Queries from bodytext. Return the output. * * @return string * @throws \CodeException * @throws \DbException * @throws \DownloadException * @throws \PhpOffice\PhpSpreadsheet\Exception * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception * @throws \Twig\Error\LoaderError * @throws \Twig\Error\RuntimeError * @throws \Twig\Error\SyntaxError * @throws \UserFormException * @throws \UserReportException */ private function doReport() { // Session Expire happens quite late, cause it can be configured per form. Session::checkSessionExpired($this->store->getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM)); $report = new Report($this->t3data, $this->evaluate, $this->phpUnit); $html = ''; $beUserLoggedIn = $this->store->getVar(TYPO3_BE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX); if ($beUserLoggedIn && $this->inlineReport) { $html .= $this->buildInlineReport($this->t3data[T3DATA_UID] ?? null, $this->t3data[T3DATA_REPORT_PATH_FILENAME] ?? null, $this->t3data[T3DATA_BODYTEXT_RAW] ?? '', $this->t3data[T3DATA_HEADER] ?? ''); } $html .= $report->process($this->t3data[T3DATA_BODYTEXT]); return $html; } /** * Constructs a form to directly edit qfq content elements inline. * * @param int|null $uid * @param string $reportPathFileNameFull * @param string $bodytext * @param string $header * @return string - the html code * @throws \CodeException * @throws \UserFormException */ public static function buildInlineReport(?int $uid, ?string $reportPathFileNameFull, string $bodytext, string $header): string { if ($uid === null) { return ''; } $icon = Support::renderGlyphIcon(GLYPH_ICON_TASKS); $showFormJs = '$("#tt-content-edit-' . $uid . '").toggleClass("hidden")'; $toggleBtn = Support::wrapTag("", $icon); $saveBtnAttributes = Support::doAttribute('class', 'btn btn-default') . Support::doAttribute('id', "tt-content-save-$uid") . Support::doAttribute('type', 'submit') . Support::doAttribute('style', 'float:right; margin:-5px;') . Support::doAttribute('title', 'Save & Reload'); $saveBtnIcon = Support::renderGlyphIcon(GLYPH_ICON_CHECK); $saveBtn = Support::wrapTag("