* r= (table.id for a single record form) * keySemId,keySemIduser * */ /** * Class Qfq * @package qfq */ class QuickFormQuery { /** * @var \qfq\Store instantiated class */ protected $store = null; /** * @var Database instantiated class */ protected $db = null; /** * @var Evaluate instantiated class */ protected $eval = null; protected $formSpec = array(); protected $feSpecAction = array(); // Form Definition: copy of the loaded form protected $feSpecNative = array(); // FormEelement Definition: all formElement.class='action' of the loaded form /** * @var array */ private $t3data = array(); // FormEelement Definition: all formElement.class='native' of the loaded form /** * @var bool */ private $phpUnit = false; /** * @var Session */ private $session = null; /* * 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 * * @throws CodeException * @throws UserFormException */ public function __construct(array $t3data = array(), $phpUnit = false) { $this->phpUnit = $phpUnit; mb_internal_encoding("UTF-8"); $this->session = Session::getInstance($phpUnit); // Refresh the session even if no new data saved. Session::set('LAST_ACTIVITY', time()); set_error_handler("\\qfq\\ErrorHandler::exception_error_handler"); if (!isset($t3data[T3DATA_BODYTEXT])) { $t3data[T3DATA_BODYTEXT] = ''; } if (!isset($t3data[T3DATA_UID])) { $t3data[T3DATA_UID] = 0; } $btp = new BodytextParser(); $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]); $this->t3data = $t3data; $bodytext = $this->t3data[T3DATA_BODYTEXT]; $this->store = Store::getInstance($bodytext, $phpUnit); $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3); $this->db = new Database(); $this->eval = new Evaluate($this->store, $this->db); $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM); $updateDb = new DatabaseUpdate($this->db); $updateDb->checkNupdate($dbUpdate); $this->store->systemStoreUpdate(); // Do this after the DB-update } /** * Returns the defined forwardMode and set forwardPage * * @return array */ public function getForwardMode() { $forwardPage = $this->eval->parse($this->formSpec[F_FORWARD_PAGE]); if ($this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_URL_SIP) { $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL); // 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. $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY; } return ([ API_REDIRECT => $this->formSpec[F_FORWARD_MODE], API_REDIRECT_URL => $forwardPage, ]); } /** * Main entrypoint for display content: a) form and/or b) report * * @return string */ public function process() { $html = ''; 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); $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(); } $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 defintions. */ 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; } } } /** * 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 UserFormException */ private function doForm($formMode) { $data = ''; $foundInStore = ''; // Fill STORE_FORM if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) { $fillStoreForm = new FillStoreForm(); $fillStoreForm->process(); } $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO); $this->setParameterLanguageFieldName(); $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore); if ($formName === false && $formMode !== FORM_DELETE) { // No form found: do nothing return ''; } if ($formName !== false) { // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename). $sipFound = $this->validateForm($foundInStore, $formMode); } 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; } // 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 (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) { $this->store->createSipAfterFormLoad($formName); } if ($this->store->getVar('id', STORE_BEFORE) === false) { $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE); } // Check (and release) dirtyRecord. if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) { $dirty = new Dirty(); $answer = $dirty->checkDirtyAndRelease($formMode, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS], $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $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 an foreign exclusive record lock - show form in F_MODE_READONLY mode. if ($formMode === FORM_LOAD) { $dirty = new Dirty(); $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] = F_MODE_READONLY; } } if ($formMode === FORM_DELETE) { $build = new Delete(); } else { $this->store->fillStoreTableDefaultColumnType($this->formSpec[F_TABLE_NAME]); switch ($this->formSpec['render']) { case 'plain': $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative); break; case 'table': $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative); break; case 'bootstrap': $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative); break; default: throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); } } $formAction = new FormAction($this->formSpec, $this->db, $this->phpUnit); switch ($formMode) { case FORM_LOAD: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); $data = $build->process($formMode); $data = Support::wrapTag("
formSpec[F_BS_COLUMNS] . "'>", $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($formMode); $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); $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_DELETE); break; case FORM_SAVE: $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP); // Action: Before $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE); // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten. $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_RECORD); // SAVE $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative); $rc = $save->process(); // Reload fresh saved record and fill STORE_RECORD with it. $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD); $save->processAllUploads($rc); // Action: After $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE); if ($status != ACTION_ELEMENT_NO_CHANGE) { // Reload fresh saved record and fill STORE_RECORD with it. $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD); } // Action: Paste $this->pasteClipboard($this->formSpec[F_ID], $formAction); // Action: Sendmail $formAction->elements($rc, $this->feSpecAction, FE_TYPE_SENDMAIL); // $htmlElementNameIdZero = false; $getJson = true; // Retrieve current STORE_SIP. $sipArray = $this->store->getStore(STORE_SIP); if ($sipArray[SIP_RECORD_ID] == 0 && API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX)) { // if ($sipArray[SIP_RECORD_ID] == 0 ) { if ($this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL && $this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL_SKIP_HISTORY && $this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL_SIP ) { $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc); } $getJson = false; } if ($getJson) { // Retrieve FE Values as JSON // $data['form-update']=... // $data = $build->process($formMode, $htmlElementNameIdZero); $data = $build->process($formMode); } break; default: throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN); } if (is_array($data)) { // $data['element-update']=... $data = $this->groupElementUpdateEntries($data); } return $data; } /** * 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 */ 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->db->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; } /** * 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 * @param int $recordId * @param string $foundInStore * * @return bool|string if found the formName, else 'false'. * @throws CodeException * @throws DbException * @throws UserFormException */ private function loadFormSpecification($mode, $recordId, &$foundInStore = '') { // formName if (false === ($formName = $this->getFormName($mode, $foundInStore))) { return false; } if (!$this->db->existTable('Form')) { throw new UserFormException("Table 'Form' not found", ERROR_MISSING_TABLE); } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM); // Check if there is a recordId specified in Bodytext - as variable or query. $rTmp = $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3, SANITIZE_ALLOW_ALL); if (false !== $rTmp && !is_int($rTmp)) { $rTmp = $this->eval->parse($rTmp); $this->store->setVar(CLIENT_RECORD_ID, $rTmp, STORE_TYPO3); } // Load form $form = $this->db->sql("SELECT * FROM Form AS f WHERE f." . F_NAME . " LIKE ? AND f.deleted='no'", ROW_EXPECT_1, [$formName], 'Form not found or multiple forms with the same name.'); $form = $this->modeCleanFormConfig($mode, $form); // Save specific elements to be expanded later. $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]); $form[F_FORWARD_PAGE] = ''; $formSpec = $this->eval->parseArray($form); HelperFormElement::explodeParameter($formSpec, F_PARAMETER); $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM); $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName); $formSpec = $this->syncSystemFormConfig($formSpec); $formSpec = $this->initForm($formSpec); $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]; $this->formSpec = $formSpec; // this is needed for filling templateGroup records with their default values $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_RECORD); // Clear $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM); // FE: Action $this->feSpecAction = $this->db->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->db->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec); break; case FORM_SAVE: case FORM_UPDATE: // $this->feSpecNative = $this->db->getNativeFormElements(SQL_FORM_ELEMENT_ALL_CONTAINER, ['no', $this->formSpec["id"], 'native'], $this->formSpec); $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec["id"]], $this->formSpec); break; case FORM_DELETE: $this->feSpecNative = array(); break; default: break; } $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName); return $formName; } /** * 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 \qfq\CodeException * @throws \qfq\DbException */ public function getNativeFormElements($sql, array $param, $formSpec) { $feSpecNative = $this->db->sql($sql, ROW_REGULAR, $param); $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative); // 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 */ 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->eval->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]); $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 * @param string $foundInStore * * @return bool|string Formname (Form.name) or FALSE (if no formname found) * @throws CodeException * @throws UserFormException */ public function getFormName($mode, &$foundInStore = '') { $dummy = array(); switch ($mode) { case FORM_LOAD: $store = STORE_TYPO3; break; case FORM_SAVE: case FORM_UPDATE: case FORM_DELETE: $store = STORE_SIP; break; default: throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE); } $storeFormName = $this->store->getVar(SIP_FORM, $store, '', $foundInStore); $formName = $this->eval->parse($storeFormName, 0, $dummy, $foundInStore); // if($mode===FORM_DELETE && $formName===false) { // return ""; // } // If the formname is '': no formname name. if ($formName === '' || $foundInStore === '') return false; // If the formname is surrounded by single ticks: the token (typically 'form') has not been replaced by a value. if ($formName[0] === "'" && $formName[strlen($formName) - 1] === "'") { 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 */ private function modeCleanFormConfig($mode, array $form) { switch ($mode) { case FORM_DELETE: $form[F_TITLE] = ''; break; default: break; } 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.ini or if note 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 inputfield. * * @param array $formSpec * * @return array */ 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, ]; // By definition: existing vars which are empty, means: EMPTY - do not use any default! // But: if these variables are table columns, they always exist. For those: empty value means 'not set' - unset those. foreach ([F_BS_LABEL_COLUMNS, F_BS_INPUT_COLUMNS, F_BS_NOTE_COLUMNS, F_ESCAPE_TYPE_DEFAULT] as $key) { if ($formSpec[$key] == '') { unset ($formSpec[$key]); } } foreach ($keys as $key) { if (isset($formSpec[$key])) { $this->store->setVar($key, $formSpec[$key], STORE_SYSTEM); } else { // if not found set '' $formSpec[$key] = $this->store->getVar($key, STORE_SYSTEM . STORE_EMPTY); } } return $formSpec; } /** * Set form parameter which are expected to exist. * * @param array $formSpec * * @return array */ private function initForm(array $formSpec) { 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_MODE, ''); if ($formSpec[F_MODE] == '' && $this->store->getVar(F_MODE_GLOBAL, STORE_SIP . STORE_CLIENT, SANITIZE_ALLOW_ALNUMX) == FE_MODE_READONLY) { $formSpec[F_MODE] = F_MODE_READONLY; } if ($formSpec[F_MODE] == 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); } return $formSpec; } /** * Check if loading of the given form is permitted. If not, throw an exception. * * @param string $formNameFoundInStore * @param string $formMode * * @return bool 'true' if SIP exists, else 'false' * @throws \qfq\CodeException * @throws \qfq\UserFormException * @internal param $foundInStore */ private function validateForm($formNameFoundInStore, $formMode) { // Retrieve record_id either from SIP (prefered) or via URL $r = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT, '', $recordIdFoundInStore); // Set missing 'r'. 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); } } 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); default: throw new CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE); } // Form Definition valid? if ($this->formSpec['multiMode'] !== 'none' && $this->formSpec['multiSql'] === '') { throw new UserFormException("MultiMode selected, but MultiSQL missing", ERROR_MULTI_SQL_MISSING); } if ($formMode !== FORM_DELETE) { $sipArray = $this->store->getStore(STORE_SIP); // Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId' $param = explode(',', $this->formSpec[F_REQUIRED_PARAMETER]); foreach ($param AS $name) { $name = explode('#', $name, 2); $name = trim($name[0]); if ($name === '') { continue; } if (!isset($sipArray[$name])) { throw new UserFormException("Missing required SIP parameter: $name", ERROR_MISSING_REQUIRED_PARAMETER); } } } return $sipFound; } /** * Load record $id from $table and saves them in $store * * @param string $table tablename from where to load record wiht $recordId * @param string $recordId record ID of current record * @param string $store name of store where to save the record * * @throws CodeException * @throws DbException * @throws UserFormException */ private function fillStoreWithRecord($table, $recordId, $store = STORE_RECORD) { if ($recordId !== false && $recordId > 0) { $record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]); $this->store->setStore($record, $store, true); } } /** * 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 (count($data) > 0) { $collect[API_FORM_UPDATE][] = $data; } } return $collect; } /** * Process the SQL Queries from bodytext. Return the output. * * @return string */ private function doReport() { $report = new Report($this->t3data, $this->eval, $this->phpUnit); $html = $report->process($this->t3data['bodytext']); return $html; } /** * Save the current form. * * @return string * @throws CodeException * @throws UserFormException */ public function saveForm() { $json = $this->doForm(FORM_SAVE); return $json; } /** * Update FormElements and form values. Receives the current form values via POST. * * @return array * @throws CodeException */ public function updateForm() { $json = $this->doForm(FORM_UPDATE); return $json; } /** * Delete a record (tablename and recordid are given) or process a 'delete form' * * @return array * @throws CodeException */ public function delete() { return $this->doForm(FORM_DELETE); } /** * Based on the given SIP, create a new uniqe SIP by copying the relevant old params and taking the new recordId.. * * @param array $sipArray * @param int $recordId */ private function newRecordCreateSip(array $sipArray, $recordId) { $tmpParam = array(); foreach ($sipArray as $key => $value) { switch ($key) { case SIP_SIP: case SIP_URLPARAM: case SIP_TABLE: continue; // do not copy these params to the new SIP case SIP_RECORD_ID: // set the new recordId $tmpParam[SIP_RECORD_ID] = $recordId; break; default: // copy further vars stored in old SIP (form, maybe default values) $tmpParam[$key] = $value; break; } } // Construct fake urlparam $tmpUrlparam = OnArray::toString($tmpParam); // Create a SIP which has never been passed by URL - further processing might expect this to exist. $sip = store::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP); $this->store->setVar(CLIENT_SIP, $sip, STORE_CLIENT); // Overwrite SIP Store $tmpParam[SIP_SIP] = $sip; $this->store->setStore($tmpParam, STORE_SIP, true); } /** * @return string */ private function getModalCode() { $code = << EOF; return $code; } }