formSpec = $formSpec; $this->feSpecAction = $feSpecAction; $this->feSpecNative = $feSpecNative; $this->feSpecNativeRaw = $feSpecNativeRaw; $this->store = Store::getInstance(); $this->db = new Database($formSpec[F_DB_INDEX]); $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 * @throws UserFormException */ 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 */ /** * @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 $tableName * @param array $values * * @return int last insert id * @throws CodeException * @throws DbException * @throws UserFormException */ 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 * @throws UserFormException */ 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); } $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. * @param $recordId * @throws CodeException * @throws DbException * @throws UserFormException * @throws UserReportException */ 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); $pathFileNameTmp = empty($pathFileName) ? $primaryRecord[$column] : $pathFileName; // Get latest file information $vars = HelperFile::getFileStat($pathFileNameTmp); $this->store->appendToStore($vars, STORE_VAR); // If given: fire a sqlBefore query $this->evaluate->parse($formElement[FE_SQL_BEFORE]); // Upload Type: Simple or Advanced // If (isset($primaryRecord[$column])) { - see #5048 - isset does not deal correctly with NULL! if (array_key_exists($column, $primaryRecord)) { // 'Simple Upload': no special action needed, just process the current (maybe modified) value. if ($pathFileName !== false) { $newValues[$column] = $pathFileName; if (isset($primaryRecord[COLUMN_FILE_SIZE])) { $newValues[COLUMN_FILE_SIZE] = $vars[VAR_FILE_SIZE]; } if (isset($primaryRecord[COLUMN_MIME_TYPE])) { $newValues[COLUMN_MIME_TYPE] = $vars[VAR_FILE_MIME_TYPE]; } } } else { // 'Advanced Upload' $this->doUploadSlave($formElement, $modeUpload); } // If given: fire a sqlAfter query $this->evaluate->parse($formElement[FE_SQL_AFTER]); } // Only used in 'Simple Upload' if (count($newValues) > 0) { $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId); } } /** * Process all Upload FormElements for the given $recordId. * After processing, &$formValues will be updated with the final filename. * * @throws CodeException * @throws UserFormException */ public function processAllImageCutFE() { foreach ($this->feSpecNative AS $formElement) { // skip non upload formElements if ($formElement[FE_TYPE] != FE_TYPE_IMAGE_CUT) { continue; } $this->extractImageDataReplaceFile($formElement); } } /** * Iterates over all FE and checks all 'required' (mode & modeSql) FE. * If a required FE is empty, throw an exception. * Take care to remove all FE with modeSql='hidden'. * * Typically, the browser does not allow a submit if a required field is empty. * * @throws CodeException * @throws DbException * @throws UserFormException * @throws UserReportException */ public function checkRequiredHidden() { $requiredOff = ($this->store->getVar(F_MODE_GLOBAL, STORE_SIP) == F_MODE_REQUIRED_OFF); $clientValues = $this->store::getStore(STORE_FORM); foreach ($this->feSpecNative AS $key => $formElement) { $this->store->setVar(SYSTEM_FORM_ELEMENT, "Column: " . $formElement[FE_NAME], STORE_SYSTEM); if (empty($formElement[FE_MODE_SQL])) { $mode = $formElement[FE_MODE]; } else { $mode = $this->evaluate->parse($formElement[FE_MODE_SQL]); $this->feSpecNative[$key][FE_MODE_SQL] = $mode; } if (!$requiredOff && $mode == FE_MODE_REQUIRED && empty($clientValues[$formElement[FE_NAME]])) { throw new UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY); } if ($mode == FE_MODE_HIDDEN) { // Removing the value from the store, forces that the value won't be stored. $this->store::unsetVar($formElement[FE_NAME], STORE_FORM); } } } /** * * @param array $formElement * @throws CodeException * @throws UserFormException */ private function extractImageDataReplaceFile(array $formElement) { // 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); } // Get original pathFileName $field = HelperFormElement::AppendFormElementNameImageCut($formElement); $pathFileName = $this->store->getVar($field, STORE_SIP); if ($pathFileName == '' || !file_exists($pathFileName)) { throw new UserFormException('Empty file or file not found: ' . $pathFileName, ERROR_IO_FILE_NOT_FOUND); } // 'data:image/png;base64,AAAFBfj42Pj4...'; $data = $this->store->getVar($formElement[FE_NAME], STORE_FORM, SANITIZE_ALLOW_ALLBUT); // Replace data by pathFileName (that is stored in DB). $this->store->setVar($formElement[FE_NAME], $pathFileName, STORE_FORM, true); if ($data == '') { return; // Nothing to do } // Split base64 encoded image: 'data:image/png;base64,AAAFBfj42Pj4...' list($type, $imageData) = explode(';', $data, 2); // $type= 'data:image/png;', $imageData='base64,AAAFBfj42Pj4...' list(, $extension) = explode('/', $type); // $type='png' list(, $imageData) = explode(',', $imageData); // $imageData='AAAFBfj42Pj4...' // If undefined: set default. BTW: Defined and empty means "no original". if (!isset($formElement[FE_IMAGE_CUT_KEEP_ORIGINAL])) { $formElement[FE_IMAGE_CUT_KEEP_ORIGINAL] = FE_IMAGE_CUT_ORIGINAL_EXTENSION; } $extSave = $formElement[FE_IMAGE_CUT_KEEP_ORIGINAL]; $pathParts = pathinfo($pathFileName); // Keep the original file? if ($extSave != '') { // In case the leading '.' is missing. if ($extSave[0] != ".") { $extSave = '.' . $extSave; } // Check if there is already an original - don't create an additional one. if (!file_exists($pathFileName . $extSave) && !file_exists($pathParts['dirname'] . $pathParts['filename'] . $extSave) ) { if (!rename($pathFileName, $pathFileName . $extSave)) { throw new UserFormException("Rename file: '$pathFileName' > '$pathFileName$extSave'", ERROR_IO_RENAME); } } } if ($extension != $pathParts['extension']) { $pathFileName .= "." . $extension; } if (false === file_put_contents($pathFileName, base64_decode($imageData))) { throw new UserFormException("Write new image failed: $pathFileName", ERROR_IO_WRITE); } $this->store->setVar($formElement[FE_NAME], $pathFileName, STORE_FORM, true); } /** * 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 DbException * @throws UserFormException * @throws UserReportException */ private function copyUploadFile(array $formElement, array $statusUpload) { $pathFileName = ''; $vars = array(); if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') { // nothing to upload: e.g. user has deleted a previous uploaded file. return ''; } $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED); 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->appendToStore(HelperFile::pathinfo($origFilename), STORE_VAR); $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]); $pathFileName = Sanitize::safeFilename($pathFileName, false, true); // Dynamically calculated pathFileName might contain invalid characters. // 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); if (!rename($srcFile, $pathFileName)) { throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME); } $this->splitUpload($formElement, $pathFileName); return $pathFileName; } /** * Check's if the file $pathFileName should be splitted in one file per page. If no: do nothing and return. * The only possible split target file format is 'svg': fileSplit=svg. * The splitted files will be saved under fileDestinationSplit=some/path/to/file.%02d.svg. A printf style token, * like '%02d', is needed to create distinguished filename's. See 'man pdf2svg' for further details. * For every created file, a record in table 'Split' is created (see splitSvg() ), storing the pathFileName of the * current page/file. * * @param array $formElement * @param $pathFileName * @throws CodeException * @throws DbException * @throws UserFormException * @throws UserReportException */ private function splitUpload(array $formElement, $pathFileName) { if (empty($formElement[FE_FILE_SPLIT]) || empty($formElement[FE_FILE_DESTINATION_SPLIT])) { return; } $fileDestinationSplit = $this->evaluate->parse($formElement[FE_FILE_DESTINATION_SPLIT]); $fileSplit = $this->evaluate->parse($formElement[FE_FILE_SPLIT]); $fileSplitTableName = $this->evaluate->parse($formElement[FE_FILE_SPLIT_TABLE_NAME]); if (empty($fileSplitTableName)) { $fileSplitTableName = $this->formSpec[F_TABLE_NAME]; } // Filetype testen: nur Dateien splitten die man auch wirklich entpacken kann switch ($fileSplit) { case FE_FILE_SPLIT_SVG: $this->splitSvg($pathFileName, $fileDestinationSplit, $fileSplitTableName); break; default: throw new UserFormException("Unknown 'fileSplit' type: " . $formElement[FE_FILE_SPLIT], ERROR_UNKNOWN_TOKEN); } } /** * Split's the PDF file $pathFileNameSrc in several SVG-file, one per page. * For every created file, a record in table 'Split' is created, storing the pathFileName to the individual file. * * @param $pathFileNameSrc * @param $fileDestinationSplit * @param $fileSplitTableName * @throws CodeException * @throws DbException * @throws UserFormException */ private function splitSvg($pathFileNameSrc, $fileDestinationSplit, $fileSplitTableName) { Support::mkDirParent($fileDestinationSplit); // Save CWD $cwd = getcwd(); // Create temporary directory $tempDir = Support::createTempDir(); $newSrc = $tempDir . DIRECTORY_SEPARATOR . QFQ_TEMP_SOURCE; $rc = copy($pathFileNameSrc, $newSrc); $rc = chdir($tempDir); // Split destination. $pathParts = pathinfo($fileDestinationSplit); if (empty($pathParts['filename']) || empty($pathParts['basename'])) { throw new UserFormException('Missing filename in ' . FE_FILE_DESTINATION_SPLIT, ERROR_MISSING_FILE_NAME); } // Extract filename from destination directory. $fileNameDest = $pathParts['basename']; // Split PDF $rc = exec('pdf2svg "' . $newSrc . '" "' . $fileNameDest . '" all'); // Array of created filenames. $files = scandir('.'); // Create DB records according to the extracted filenames. $tableName = TABLE_NAME_SPLIT; $sql = "INSERT INTO $tableName (`tableName`, `xId`, `pathFilename`, `created`) VALUES (?,?,?, NOW())"; foreach ($files as $file) { if ($file == '.' || $file == '..' || $file == QFQ_TEMP_SOURCE) { continue; } if (!empty($pathParts['dirname'])) { $fileDestination = $pathParts['dirname'] . '/' . $file; } else { $fileDestination = $file; } $rc = rename($file, Support::joinPath($cwd, $fileDestination)); // Insert records. $this->db->sql($sql, ROW_REGULAR, [$fileSplitTableName, $this->store->getVar(COLUMN_ID, STORE_RECORD), $fileDestination]); } // Pop directory $rc = chdir($cwd); // Remove duplicated source $rc = unlink($newSrc); // Remove empty directory $rc = rmdir($tempDir); } /** * Create/update or delete the slave record. * * @param array $fe * @param $modeUpload * @return int * @throws CodeException * @throws DbException * @throws UserFormException * @throws UserReportException */ 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); } $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; } return $slaveId; } }