formSpec = $formSpec; $this->feSpecAction = $feSpecAction; $this->feSpecNative = $feSpecNative; $this->feSpecNativeRaw = $feSpecNativeRaw; $this->store = Store::getInstance(); $this->db = new Database(); $this->evaluate = new Evaluate($this->store, $this->db); } /** * Starts save process. On succcess, returns forwardmode/page. * * @return int * @throws CodeException * @throws DbException * @throws UserFormException */ public function process() { $rc = 0; if ($this->formSpec['multiMode'] !== 'none') { $parentRecords = $this->db->sql($this->formSpec['multiSql']); foreach ($parentRecords as $row) { $this->store->setStore($row, STORE_PARENT_RECORD, true); $rc = $this->elements($row['_id']); } } else { $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO); $rc = $this->elements($recordId); } return $rc; } /** * Create empty FormElements based on templateGroups, for those who not already exist. * * @param array $formValues * * @return array */ private function createEmptyTemplateGroupElements(array $formValues) { foreach ($this->feSpecNative as $formElement) { switch ($formElement[FE_TYPE]) { // case FE_TYPE_EXTRA: case FE_TYPE_NOTE: case FE_TYPE_SUBRECORD: continue 2; default: break; } $feName = $formElement[FE_NAME]; if (!isset($formValues[$feName]) && $this->isMemberOfTemplateGroup($formElement)) { $formValues[$feName] = $formElement[FE_VALUE]; } } return $formValues; } /** * Check if the current $formElement is member of a templateGroup. * * @param array $formElement * @param int $depth * @return bool * @throws UserFormException */ private function isMemberOfTemplateGroup(array $formElement, $depth = 0) { $depth++; if ($depth > 15) { throw new UserFormException('FormElement nested too much (in each other - endless?): stop recursion', ERROR_FE_NESTED_TOO_MUCH); } if ($formElement[FE_TYPE] == FE_TYPE_TEMPLATE_GROUP) { return true; } if ($formElement[FE_ID_CONTAINER] == 0) { return false; } // Get the parent element $formElementArr = OnArray::filter($this->feSpecNativeRaw, FE_ID, $formElement[FE_ID_CONTAINER]); if (isset($formElementArr[0])) { return $this->isMemberOfTemplateGroup($formElementArr[0], $depth); } return false; // This should not be reached, } /** * * @param $feName * * @return bool */ private function isSetEmptyMeansNull($feName) { $fe = OnArray::filter($this->feSpecNative, FE_NAME, $feName); $flag = isset($fe[0][FE_EMPTY_MEANS_NULL]) && $fe[0][FE_EMPTY_MEANS_NULL] != '0'; return $flag; } /** * Build an array of all values which should be saved. Values must exist as a 'form value' as well as a regular * 'table column'. * * @param $recordId * * @return int record id (in case of insert, it's different from $recordId) * @throws CodeException * @throws DbException * @throws UserFormException */ public function elements($recordId) { $columnCreated = false; $columnModified = false; $newValues = array(); $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES)); $formValues = $this->store->getStore(STORE_FORM); $formValues = $this->createEmptyTemplateGroupElements($formValues); // Iterate over all table.columns. Built an assoc array $newValues. foreach ($tableColumns AS $column) { // Never save a predefined 'id': autoincrement values will be given by database.. if ($column === COLUMN_ID) { continue; } // Skip Upload Elements: those will be processed later. if ($this->isColumnUploadField($column)) { continue; } if ($column === COLUMN_CREATED) { $columnCreated = true; } if ($column === COLUMN_MODIFIED) { $columnModified = true; } // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement. if (!isset($formValues[$column])) { continue; } $this->store->setVar(SYSTEM_FORM_ELEMENT, "Column: $column", STORE_SYSTEM); // Check if an empty string has to be converted to null. if (isset($formValues[$column]) && $formValues[$column] == '' && $this->isSetEmptyMeansNull($column)) { $formValues[$column] = null; } else { Support::setIfNotSet($formValues, $column); } $newValues[$column] = $formValues[$column]; } if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) { $newValues[COLUMN_MODIFIED] = date('YmdHis'); } if ($recordId == 0) { if ($columnCreated && !isset($newValues[COLUMN_CREATED])) { $newValues[COLUMN_CREATED] = date('YmdHis'); } $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues); } else { $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId); $rc = $recordId; } return $rc; } /* * Checks if there is a formElement with name '$feName' of type 'upload' * * @param $feName * @return bool */ private function isColumnUploadField($feName) { foreach ($this->feSpecNative AS $formElement) { if ($formElement[FE_NAME] === $feName && $formElement[FE_TYPE] == 'upload') return true; } return false; } /** * Insert new record in table $this->formSpec['tableName']. * * @param array $values * * @return int last insert id * @throws DbException */ public function insertRecord($tableName, array $values) { if (count($values) === 0) return 0; // nothing to write, last insert id=0 $paramList = str_repeat('?, ', count($values)); $paramList = substr($paramList, 0, strlen($paramList) - 2); $columnList = '`' . implode('`, `', array_keys($values)) . '`'; $sql = 'INSERT INTO ' . $tableName . ' ( ' . $columnList . ' ) VALUES ( ' . $paramList . ' )'; $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); return $rc; } /** * @param string $tableName * @param array $values * @param int $recordId * * @return bool|int false if $values is empty, else affectedrows * @throws CodeException * @throws DbException */ public function updateRecord($tableName, array $values, $recordId) { if (count($values) === 0) return 0; // nothing to write, 0 rows affected if ($recordId === 0) { throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN); } // $paramList = str_repeat('?, ', count($values)); // $paramList = substr($paramList, 0, strlen($paramList) - 2); $sql = 'UPDATE `' . $tableName . '` SET '; foreach ($values as $column => $value) { $sql .= '`' . $column . '` = ?, '; } $sql = substr($sql, 0, strlen($sql) - 2) . ' WHERE id = ?'; $values[] = $recordId; $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values)); return $rc; } /** * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the * final filenames. * */ public function processAllUploads($recordId) { $sip = new Sip(false); $newValues = array(); $formValues = $this->store->getStore(STORE_FORM); $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table. foreach ($this->feSpecNative AS $formElement) { // skip non upload formElements if ($formElement[FE_TYPE] != FE_TYPE_UPLOAD) { continue; } $formElement = HelperFormElement::initUploadFormElement($formElement); if (isset($formElement[FE_FILL_STORE_VAR])) { $this->store->appendToStore($formElement[FE_FILL_STORE_VAR], STORE_VAR); } // Preparation for Log, Debug $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM); $column = $formElement[FE_NAME]; $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload); // Upload Type: Simple or Advanced if (isset($primaryRecord[$column])) { // 'Simple Upload': no special action needed, just process the current (maybe modifired) value. if ($pathFileName !== false) { $newValues[$column] = $pathFileName; } } else { // 'Advanced Upload' $this->doUploadSlave($formElement, $modeUpload); } } // Only used in 'Simple Upload' if (count($newValues) > 0) { $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId); } } /** * Process upload for the given Formelement. If necessary, delete a previous uploaded file. * Calculate the final path/filename and move the file to the new location. * * Check also: doc/CODING.md * * @param array $formElement FormElement 'upload' * @param string $sipUpload SIP * @param Sip $sip * @param string $modeUpload UPLOAD_MODE_UNCHANGED | UPLOAD_MODE_NEW | UPLOAD_MODE_DELETEOLD | * UPLOAD_MODE_DELETEOLD_NEW * * @return false|string New pathFilename or false on error * @throws CodeException * @throws UserFormException * @internal param $recordId */ private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) { $flagDelete = false; $modeUpload = UPLOAD_MODE_UNCHANGED; // Status information about upload file $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA); if ($statusUpload === false) { return false; } // Take care the necessary target directories exist. $cwd = getcwd(); $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM); if ($cwd === false || $sitePath === false || !chdir($sitePath)) { throw new UserFormException("getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed.", ERROR_IO_CHDIR); } // Delete existing old file. if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') { $arr = $sip->getVarsFromSip($sipUpload); $oldFile = $arr[EXISTING_PATH_FILE_NAME]; if (file_exists($oldFile)) { if (!unlink($oldFile)) { throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK); } } $flagDelete = ($oldFile != ''); } // Set $modeUpload if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') { $modeUpload = $flagDelete ? UPLOAD_MODE_DELETEOLD_NEW : UPLOAD_MODE_NEW; } else { $modeUpload = $flagDelete ? UPLOAD_MODE_DELETEOLD : UPLOAD_MODE_UNCHANGED; } $pathFileName = $this->copyUploadFile($formElement, $statusUpload); chdir($cwd); // Delete current used uniq SIP $this->store->setVar($sipUpload, array(), STORE_EXTRA); return $pathFileName; } /** * Copy uploaded file from temporary location to final location. * * Check also: doc/CODING.md * * @param array $formElement * @param array $statusUpload * * @return array|mixed|null|string * @throws CodeException * @throws UserFormException */ private function copyUploadFile(array $formElement, array $statusUpload) { $pathFileName = ''; if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') { // nothing to upload: e.g. user has deleted a previous uploaded file. return ''; } if (isset($formElement[FE_FILE_DESTINATION])) { // Provide variable 'filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME]. $origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]); $this->store->setVar(VAR_FILENAME, $origFilename, STORE_VAR); $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]); // Saved in store for later use during 'Advanced Upload'-post processing $this->store->setVar(VAR_FILE_DESTINATION, $pathFileName, STORE_VAR); } if ($pathFileName === '') { throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME); } if (file_exists($pathFileName)) { if (isset($formElement[FE_FILE_REPLACE_MODE]) && $formElement[FE_FILE_REPLACE_MODE] == FE_FILE_REPLACE_MODE_ALWAYS) { if (!unlink($pathFileName)) { throw new UserFormException('Copy upload failed - file exist and unlink() failed: ' . $pathFileName, ERROR_IO_UNLINK); } } else { throw new UserFormException('Copy upload failed - file already exist: ' . $pathFileName, ERROR_IO_FILE_EXIST); } } Support::mkDirParent($pathFileName); $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); if (!rename($srcFile, $pathFileName)) { throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME); } return $pathFileName; } /** * Create/update or delete the slave record. * * @param array $fe * @param bool $flagNewUpload * * @return int * @throws CodeException * @throws UserFormException */ private function doUploadSlave(array $fe, $modeUpload) { $sql = ''; $flagUpdateSlaveId = false; $flagSlaveDeleted = false; if (!isset($fe[FE_SLAVE_ID])) { throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION); } // Get the slaveId $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID])); // Store the slaveId: it's used and replaced in the update statement. $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true); $mode = ($slaveId == '0') ? 'I' : 'U'; // I=Insert, U=Update $mode .= ($modeUpload == UPLOAD_MODE_NEW || $modeUpload == UPLOAD_MODE_DELETEOLD_NEW) ? 'N' : ''; // N=New File, '' if no new file. $mode .= ($modeUpload == UPLOAD_MODE_DELETEOLD) ? 'D' : ''; // Delete slave record only if there is no new and not 'unchanged'. switch ($mode) { case 'IN': $sql = $fe[FE_SQL_INSERT]; $flagUpdateSlaveId = true; break; case 'UN': $sql = $fe[FE_SQL_UPDATE]; break; case 'I': case 'U': $sql = ''; // no old file and no new file. break; case 'UD': $sql = $fe[FE_SQL_DELETE]; $flagSlaveDeleted = true; break; default: throw new CodeException('Unknown mode: ' . $mode, ERROR_UNKNOWN_MODE); } // If given: fire a sqlBefore query $this->evaluate->parse($fe[FE_SQL_BEFORE]); $rc = $this->evaluate->parse($sql); // Check if the slave record has been deleted: if yes, set slaveId=0 if ($flagSlaveDeleted && $rc > 0) { $rc = 0; $flagUpdateSlaveId = true; } if ($flagUpdateSlaveId) { // Store the slaveId: it's used and replaced in the update statement. $this->store->setVar(VAR_SLAVE_ID, $rc, STORE_VAR, true); $slaveId = $rc; } // If given: fire a sqlAfter query $this->evaluate->parse($fe[FE_SQL_AFTER]); return $slaveId; } }