<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 4/25/16
 * Time: 10:39 PM
 */

namespace IMATHUZH\Qfq\Core;


use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;

/**
 * Class File
 * @package qfq
 */
class File {

    private $uploadErrMsg = array();
    public $imageUploadFilePath = null;
    public $uniqueFileId = null;
    public $groupId = null;
    public $sipTmp = null;
    public $fileIndex = null;
    private $db = null;
    private $dbIndexData = null;
    private $dbIndexQfq = null;
    private $dbIndexFilePond = null;
    private $dbArray = null;
    private $sip = null;


    /**
     * @var Store
     */
    private $store = null;

    /**
     * @var Session
     */
    private $session = null;

    private $qfqLogPathFilenameAbsolute = '';
    private ?Link $link = null;

    /**
     * @param bool|false $phpUnit
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct($phpUnit = false) {

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
        $this->session = Session::getInstance($phpUnit);

        $this->store = Store::getInstance('', $phpUnit);
        $this->qfqLogPathFilenameAbsolute = Path::absoluteQfqLogFile();

        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);

        $this->link = new Link(new Sip());

        // Default is qfq index
        $this->dbIndexFilePond = $this->dbIndexQfq;
        $dbIndexSip = $this->store->getVar(TOKEN_DB_INDEX, STORE_SIP . STORE_EMPTY);
        if (in_array($dbIndexSip, [$this->dbIndexData, $this->dbIndexQfq])) {
            $this->dbIndexFilePond = $dbIndexSip;
        }
        $this->dbArray[$this->dbIndexFilePond] = new Database($this->dbIndexFilePond);

        $this->sip = $this->store->getSipInstance();

        $this->uploadErrMsg = [
            UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini",
            UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form",
            UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded",
            UPLOAD_ERR_NO_FILE => "No file was uploaded",
            UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder",
            UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk",
            UPLOAD_ERR_EXTENSION => "File upload stopped by extension",
        ];
    }

    /**
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \InfoException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function process() {

        $action = $this->store->getVar(FILE_ACTION, STORE_CLIENT . STORE_SIP, SANITIZE_ALLOW_ALNUMX);
        $sipUpload = $this->store->getVar(SIP_SIP, STORE_SIP);
        if ($sipUpload === false && $action != FILE_ACTION_IMAGE_UPLOAD) {

            if (empty($_FILES)) {
                throw new \UserFormException('Missing $_FILES[] - the whole request seems broken', ERROR_UPLOAD_FILES_BROKEN);
            }

            throw new \UserFormException('SIP invalid: ' . $sipUpload, ERROR_SIP_INVALID);
        }

        // Throws an exception if content is too big - if content is bigger than 'post_max_size', the POST is lost together with the PHP Upload error message.
        // This doesn't count the actual filesize. Counts the whole POST which can lead to discrepancy with the client side filesize check
        // Maybe this is needed for case image upload (tinymce ?)
        $size = $_SERVER['CONTENT_LENGTH'] ?? 0;
        // Get actual filesize from $_FILES if exists
        if (!empty($_FILES)) {
            if (isset($_FILES['filepond'])) {
                $size = $_FILES['filepond']['size'];
            } else if (isset($_FILES['size'])) {
                $size = $_FILES['size'];
            }
        }

        if ($action != FILE_ACTION_IMAGE_UPLOAD) {
            $this->checkMaxFileSize($size);
        }

        $keyname = isset($_FILES[FILES_FILEPOND]) ? FILES_FILEPOND : UPLOAD_VALUE_FILE;
        $tmpFile = $_FILES[$keyname][FILES_TMP_NAME] ?? '';
        $fileType = $_FILES[$keyname][FILES_TYPE] ?? '';

        $this->checkMaxImageDimension($tmpFile, $fileType);

        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA, SANITIZE_ALLOW_ALL);
        if ($statusUpload === false) {
            $statusUpload = array();
        }
        $statusUpload[UPLOAD_SIP_DOWNLOAD_KEY] = $this->store->getVar(UPLOAD_SIP_DOWNLOAD_KEY, STORE_SIP);

        if ($fileType === 'application/pdf') {
            $this->handleUploadProtection($tmpFile);
        }
        switch ($action) {
            case FILE_ACTION_UPLOAD:
            case FILE_ACTION_UPLOAD_2:
                $this->doUpload($sipUpload, $statusUpload);
                break;
            case MULTI_UPLOAD_API_ACTION_UPLOAD:
                $this->doUploadMulti($sipUpload);
                break;
            case FILE_ACTION_DELETE:
                $this->doDelete($sipUpload, $statusUpload);
                break;
            case MULTI_UPLOAD_API_ACTION_DELETE:
                $this->doDeleteMulti($sipUpload);
                break;
            case FILE_ACTION_IMAGE_UPLOAD:
                $this->imageUploadFilePath = $this->doImageUpload();
                break;
            default:
                throw new \UserFormException("Unknown FILE_ACTION: $action", ERROR_UPLOAD_UNKNOWN_ACTION);
        }
    }

    /**
     * Checks the max file size, defined per FormElement.
     * Only possible if there is a valid SIP Store.
     *
     * @param $size
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function checkMaxFileSize($size) {

        // Set during form build. Defensive parse: older SIPs may still carry strings like '5MB'.
        $maxFileSize = Support::returnBytes($this->store->getVar(FE_FILE_MAX_FILE_SIZE, STORE_SIP . STORE_ZERO));

        if ($size >= $maxFileSize && $maxFileSize != 0) {
            throw new \UserFormException('File too big. Size: ' . $size . ' Bytes. Max allowed: ' . $maxFileSize . ' Bytes.', ERROR_UPLOAD_TOO_BIG);
        }
    }

    /**
     * doUpload
     *
     * @param string $sipUpload
     * @param array $statusUpload
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function doUpload($sipUpload, array $statusUpload) {

        $action = $this->store->getVar(FILE_ACTION, STORE_SIP, SANITIZE_ALLOW_ALNUMX);

        // New upload
        $newArr = reset($_FILES);
        // Merge new upload date to existing status information
        $statusUpload = array_merge($statusUpload, $newArr);
        HelperFile::fixMimeTypeHeif($statusUpload['tmp_name'], $statusUpload['type']);

        if ($statusUpload[FILES_ERROR] !== UPLOAD_ERR_OK) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Upload: Error', ERROR_MESSAGE_TO_DEVELOPER => $this->uploadErrMsg[$newArr[FILES_ERROR]]]),
                ERROR_UPLOAD);
        }

        Logger::logMessageWithPrefix(UPLOAD_LOG_PREFIX . ': File under ' . $statusUpload['tmp_name'], $this->qfqLogPathFilenameAbsolute);

        if (!empty($sipUpload)) {
            $this->checkMaxFileSize($statusUpload[FILES_SIZE]);
        }

        $accept = $this->store->getVar(FE_FILE_MIME_TYPE_ACCEPT, STORE_SIP . STORE_EMPTY);
        if ($accept != '' && !HelperFile::checkFileType($statusUpload['tmp_name'], $statusUpload['name'], $accept)) {
            throw new \UserFormException('Filetype not allowed. Allowed: ' . $accept, ERROR_UPLOAD_FILE_TYPE);
        }

        // rename uploaded file: ?.cached
        $filenameCached = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);

        // Remember: PHP['upload_tmp_dir']='' means '/tmp' AND upload process is CHROOT to /tmp/systemd-private-...-apache2.service-../
        error_clear_last();
        if ($action === FILE_ACTION_UPLOAD_2) {
            // Generate a unique file ID
            $this->uniqueFileId = $_POST[UPLOAD_ID];
            $fullPath = $_POST[UPLOAD_PATH_FILE_NAME];
            $pathDefault = $_POST[UPLOAD_PATH_DEFAULT];
            $groupId = $_POST['groupId'];
            $recordData = $this->store->getVar(UPLOAD_RECORD_DATA, STORE_SIP);
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            $filename = $statusUpload[FILES_NAME];

            if ($pathDefault === 'undefined') {
                $filenameDummy = basename($fullPath);
                $fileParts = pathinfo($filenameDummy);

                if (isset($fileParts['extension'])) {
                    $filename = $filenameDummy;
                    $fullPath = dirname($fullPath) . '/';
                } else if (substr($fullPath, -1) !== '/') {
                    $fullPath .= '/';
                }
            }

            if ($this->uniqueFileId == $groupId) {
                $this->uniqueFileId = 0;
            }
            $filename = Sanitize::safeFilename($filename);

            $fullPath = Path::absoluteApp($fullPath);
            if (str_contains($fullPath, FILE_NAME_UNIQUE_VAR)) {
                $fullPath = HelperFile::base64EncodeUniqueFileName(rtrim($fullPath, '/'), $filename, FILE_NAME_UNIQUE_RETURN_PATH);
                $fullPath = $fullPath[FILE_NAME_UNIQUE_KEY];
            } else {
                $changedFileName = HelperFile::getUniqueFileName($fullPath, $filename);
                $fullPath = $fullPath . $changedFileName;
            }

            HelperFile::mkDirParent($fullPath);
            if (!move_uploaded_file($newArr[FILES_TMP_NAME], $fullPath)) {
                $msg = error_get_last();
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => 'Upload: Error', ERROR_MESSAGE_TO_DEVELOPER => $msg]),
                    ERROR_UPLOAD_FILE_TYPE);
            }

            if ($this->uniqueFileId == 0) {
                $insertColumns = '';
                $insertValues = [];
                $prepareQuestions = '';
                if ($recordData != '') {
                    $recordData = json_decode($recordData);
                    foreach ($recordData as $key => $value) {
                        $insertColumns .= ',' . $key;
                        $insertValues[] = $value;
                        $prepareQuestions .= ',?';
                    }
                }
                $params = array_merge([$fullPath, $groupId, $statusUpload[FILES_SIZE], $statusUpload[FILES_TYPE], 0], $insertValues);
                // Do database insert with unique file ID and pathFileName
                $dbName = $this->store->getVar(SYSTEM_DB_NAME_QFQ, STORE_SYSTEM);
                $sql = "INSERT INTO `$dbName`.`$table` (pathFileName, uploadId, fileSize, mimeType, ord  $insertColumns)
            VALUES (?,?,?,?,? $prepareQuestions)";
                $this->uniqueFileId = $this->dbArray[$this->dbIndexFilePond]->sql($sql, ROW_REGULAR, $params, "Creating FileUpload failed.");

                if ($groupId == 0) {
                    $groupId = $this->uniqueFileId;
                    $sqlUpdate = "UPDATE `$dbName`.`$table` SET uploadId = ? WHERE id = ?";
                    $this->dbArray[$this->dbIndexFilePond]->sql($sqlUpdate, ROW_REGULAR, [$this->uniqueFileId, $this->uniqueFileId], "Updating FileUpload failed.");
                }
            }
            $this->groupId = $groupId;
            $this->logUpload($this->uniqueFileId);
        } else {
            if (!move_uploaded_file($newArr[FILES_TMP_NAME], $filenameCached)) {
                $msg = error_get_last();
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => 'Upload: Error', ERROR_MESSAGE_TO_DEVELOPER => $msg]),
                    ERROR_UPLOAD_FILE_TYPE);
            }

            // Make currently uploaded file downloadable
            $link = new Link($this->sip, $this->dbIndexData);
            $sipDownload = $link->renderLink('', 's|d:output|M:file|r:8|F:' . $filenameCached);
            $this->sipTmp = $sipDownload;

            // Reset sipDownloadKey after fresh upload
            $this->store->setVar($statusUpload[UPLOAD_SIP_DOWNLOAD_KEY], array(), STORE_EXTRA);
        }

        $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA);
    }

    /**
     * Handle a new file upload for a multi-upload form element.
     *
     * @throws \UserFormException
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserReportException
     */
    private function doUploadMulti($sipUpload): void {
        // Retrieve Existing Session Data
        $sipStore = $this->store->getStore(STORE_SIP);
        $uploadsArray = onString::extractUploadsFromSipString($sipStore[SIP_URLPARAM]);
        // If extraction fails no uploads have been made and this is the first one
        if ($uploadsArray === false) {
            $uploadsArray = [];
        }

        $newUpload = reset($_FILES);

        HelperFile::fixMimeTypeHeif($newUpload[FILES_TMP_NAME], $newUpload[FILES_TYPE]);

        if ($newUpload[FILES_ERROR] !== UPLOAD_ERR_OK) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Upload: Error', ERROR_MESSAGE_TO_DEVELOPER => $this->uploadErrMsg[$newUpload[FILES_ERROR]]]),
                ERROR_UPLOAD);
        }

        Logger::logMessageWithPrefix(UPLOAD_LOG_PREFIX . ': File under ' . $newUpload[FILES_TMP_NAME], $this->qfqLogPathFilenameAbsolute);

        if (!empty($sipUpload)) {
            $this->checkMaxFileSize($newUpload[FILES_SIZE]);
        }

        $accept = $this->store->getVar(FE_FILE_MIME_TYPE_ACCEPT, STORE_SIP . STORE_EMPTY);

        if ($accept != '' && !HelperFile::checkFileType($newUpload[FILES_TMP_NAME], $newUpload[FILES_NAME], $accept)) {
            throw new \UserFormException('Filetype not allowed. Allowed: ' . $accept, ERROR_UPLOAD_FILE_TYPE);
        }

        // rename uploaded file: ?.cached
        $filenameCached = Support::extendFilename($newUpload[FILES_TMP_NAME], UPLOAD_CACHED);

        // Remember: PHP['upload_tmp_dir']='' means '/tmp' AND upload process is CHROOT to /tmp/systemd-private-...-apache2.service-../
        error_clear_last();

        if (!move_uploaded_file($newUpload[FILES_TMP_NAME], $filenameCached)) {
            $msg = error_get_last();
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Upload: Error', ERROR_MESSAGE_TO_DEVELOPER => $msg]),
                ERROR_UPLOAD_FILE_TYPE);
        }

        // Update Upload Information
        $newUpload[MULTI_UPLOAD_NEW_UPLOAD_FLAG] = 1;
        $uploadsArray[] = $newUpload;
        $this->fileIndex = count($uploadsArray) - 1;

        // Update the Session SIP
        $sipStore[MULTI_UPLOAD_UPLOADS_KEY] = json_encode($uploadsArray);
        $sip = OnArray::toString($sipStore);
        Session::set($sipUpload, $sip);
    }

    /**
     * @param $sipUpload
     * @param $statusUpload
     *
     * @throws \CodeException
     * @throws \UserFormException
     * @internal param string $keyStoreExtra
     */
    private function doDelete($sipUpload, $statusUpload) {
        $action = $this->store::getVar(FILE_ACTION, STORE_SIP . STORE_EMPTY);

        // Handle FilePond delete
        if ($action == FILE_ACTION_DELETE) {
            $uploadId = $_POST[UPLOAD_ID];
            $table = $this->store::getVar(SIP_TABLE, STORE_SIP . STORE_EMPTY);;
            $allowDelete = $this->store::getVar('allowDelete', STORE_SIP . STORE_EMPTY);
            $preloadedFileList = $this->store::getVar('preloadedFileIds', STORE_SIP . STORE_EMPTY);
            $preloadedFileArray = explode(',', $preloadedFileList);
            $pathFileName = '';

            // Check if uploadId exists in preloaded files and if it is allowed to delete
            if ($allowDelete == '0' && in_array($uploadId, $preloadedFileArray)) {
                return;
            }

            // Get path from database
            $sqlPath = "SELECT pathFileName FROM `$table` WHERE id = ?";
            $result = $this->dbArray[$this->dbIndexFilePond]->sql($sqlPath, ROW_EXPECT_1, [$uploadId], "File not found in database.");

            if (!empty($result[UPLOAD_PATH_FILE_NAME])) {
                $pathFileName = $result[UPLOAD_PATH_FILE_NAME];
            }

            // Delete file from database with unique file id
            $sqlPath = "DELETE FROM `$table` WHERE id = ?";
            $this->dbArray[$this->dbIndexFilePond]->sql($sqlPath, ROW_EXPECT_1, [$uploadId], "File not deleted from database.");

            // Delete file from server
            HelperFile::unlink($pathFileName);

            $this->logUpload($uploadId, true);

            $this->uniqueFileId = 0;
        } else {
            if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') {
                $file = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
                if (file_exists($file)) {
                    HelperFile::unlink($file, $this->qfqLogPathFilenameAbsolute);
                }
                $statusUpload[FILES_TMP_NAME] = '';
            }

            $statusUpload[FILES_FLAG_DELETE] = '1';
            $this->store->setVar($sipUpload, $statusUpload, STORE_EXTRA);
        }
    }

    /**
     * Handle deletion for a multi-upload operation.
     *
     * Marks the file to be deleted with the delete flag.
     * It will delete the physical file if it’s a new upload that has not been saved yet.
     * Updates the uploadSip with the altered upload array.
     *
     *
     * @throws \UserFormException
     * @throws \CodeException
     * @throws \InfoException
     */
    private function doDeleteMulti($sipUpload): void {
        // Retrieve Session Upload Data
        $sipStore = $this->store->getStore(STORE_SIP);
        $downloadsArray = onString::extractUploadsFromSipString($sipStore[SIP_URLPARAM]);
        $downloadIndex = $_POST[UPLOAD_ID] ?? false;

        if ($downloadIndex !== false && $downloadsArray) {
            // Get the specific download record based on the provided index.
            $download = $downloadsArray[$downloadIndex];

            // Check if the file is a new upload (temporary) and if the temporary file exists.
            // If both conditions are met, delete the temporary file immediately.
            if (($download[MULTI_UPLOAD_NEW_UPLOAD_FLAG] ?? 0) === 1 && file_exists($download[FILES_TMP_NAME])) {
                HelperFile::unlink($download[FILES_TMP_NAME], $this->qfqLogPathFilenameAbsolute);
            }

            $downloadsArray[$downloadIndex][MULTI_UPLOAD_DELETE_FLAG] = 1;
        } else {
            throw new \InfoException('Could not delete File');
        }

        // Update Session SIP
        $sipStore[MULTI_UPLOAD_UPLOADS_KEY] = json_encode($downloadsArray);
        $sip = OnArray::toString($sipStore);
        Session::set($sipUpload, $sip);
    }

    /**
     * Get tinyMce image upload and store it with unique filename in system. Return new filepath.
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function doImageUpload() {
        reset($_FILES);
        $temp = current($_FILES);

        $httpOrigin = Store::getVar(SYSTEM_HTTP_ORIGIN, STORE_SYSTEM, SANITIZE_ALLOW_ALL);

        if (!is_uploaded_file($temp[FILES_TMP_NAME])) {
            // Notify editor that the upload failed
            header(HTTP_500_SERVER_ERROR);
            Logger::logMessageWithPrefix('Internal error. Uploaded file not found.', Path::absoluteQfqLogFile());
            return 0;
        }

        // same-origin requests won't set an origin. If the origin is set, it must be valid.
        if ($httpOrigin === $_SERVER['HTTP_ORIGIN']) {
            header('Access-Control-Allow-Origin: ' . $httpOrigin);
        } else {
            header(HTTP_403_ORIGIN_DENIED);
            Logger::logMessageWithPrefix('Origin denied. From server given origin doesnt match with baseUrl.', Path::absoluteQfqLogFile());
            return 0;
        }

        /*
          If your script needs to receive cookies, set images_upload_credentials : true in
          the configuration and enable the following two headers.
        */
        // header('Access-Control-Allow-Credentials: true');
        // header('P3P: CP="There is no P3P policy."');

        // Sanitize input
        // Disallow 'none alphanumeric'. Allow dot or underscore and conditionally '/'.
        $temp[FILES_NAME] = Sanitize::safeFilename($temp[FILES_NAME]);

        // Verify extension
        // Split between 'Media Type' and 'Media Subtype'
        $fileMimeTypeSplitted = explode('/', $temp[FILES_TYPE], 2);
        if ($fileMimeTypeSplitted[0] !== NAME_IMAGE) {
            header(HTTP_400_INVALID_EXTENSION);
            Logger::logMessageWithPrefix('Mime type error. Invalid picture type.', Path::absoluteQfqLogFile());
            return 0;
        }

        // Base folder path should be with user input changeable -> take over sip given parameters from formElement
        $imageUploadDirUser = Store::getVar(FE_EDITOR_FILE_UPLOAD_PATH, STORE_SIP, SANITIZE_ALLOW_ALL);
        $imageUploadDirUser = base64_decode($imageUploadDirUser);

        // Get upload path from config
        $imageUploadDir = Store::getVar(SYSTEM_IMAGE_UPLOAD_DIR, STORE_SYSTEM, SANITIZE_ALLOW_ALL);

        //overwrite imageUploadDir with user given path if exists
        if ($imageUploadDirUser != '') {
            $imageUploadDir = $imageUploadDirUser;
        }

        $imageUploadDirAbsolute = Path::absoluteApp($imageUploadDir);

        // Name should be unique, add to end of name number if exists to not overwrite it
        $changedFileName = HelperFile::getUniqueFileName($imageUploadDirAbsolute, $temp[FILES_NAME]);
        $neededSlash = '';
        if (substr($imageUploadDirAbsolute, -1) !== '/') {
            $neededSlash = '/';
        }

        if (str_contains($imageUploadDirUser, FILE_NAME_UNIQUE_VAR)) {
            // Base64 Encoded ??
            $imageUploadDir = HelperFile::base64EncodeUniqueFileName($imageUploadDirUser, $temp[FILES_NAME], FILE_NAME_UNIQUE_RETURN_PATH)[FILE_NAME_UNIQUE_KEY];
            $fileToWrite = Path::absoluteApp($imageUploadDir);
            $changedFileName = '';
            $neededSlash = '';
        } else {
            $fileToWrite = $imageUploadDirAbsolute . $neededSlash . $changedFileName;
        }

        HelperFile::mkDirParent($fileToWrite);
        move_uploaded_file($temp[FILES_TMP_NAME], $fileToWrite);

        if ($this->store->getVar(FE_TINYMCE_SECURE_IMAGE_UPLOAD)) {
            return $this->link->renderLink('r:7|M:file|d|F:' . $imageUploadDir . $neededSlash . $changedFileName);
        }

        $baseUrl = Store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM, SANITIZE_ALLOW_ALL);

        // Respond to the successful upload with JSON.
        return $baseUrl . $imageUploadDir . $neededSlash . $changedFileName;
    }

    private function logUpload($uploadId, $deleteFlag = false, $uploadType = 'report'): void {
        $formName = '_uploadInReport';
        if ($uploadType === 'form') {
            $formName = '_uploadInForm';
        }

        $recordId = $uploadId;
        $pageId = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $sessionId = session_id();
        $feUser = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $clientIp = $this->store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT . STORE_EMPTY);
        $userAgent = $this->store->getVar(CLIENT_HTTP_USER_AGENT, STORE_CLIENT . STORE_EMPTY);
        $sipData = json_encode($this->store->getStore(STORE_SIP), JSON_UNESCAPED_UNICODE);
        $formData = $deleteFlag ? 'deleted' : 'uploaded';
        $formId = 0;

        $sql = "INSERT INTO `FormSubmitLog` (`formData`, `sipData`, `clientIp`, `feUser`, `userAgent`, `formId`, `formName`, `recordId`, `pageId`, `sessionId`, `created`)" .
            "VALUES (?, ?, ?, ?, ?,  ?, ?, ?, ?, ?, NOW())";

        $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $formName, $recordId, $pageId, $sessionId];

        $this->dbArray[$this->dbIndexFilePond]->sql($sql, ROW_REGULAR, $params);
        $formSubmitLogId = $this->dbArray[$this->dbIndexFilePond]->getLastInsertId();
        $this->store::setVar(EXTRA_FORM_SUBMIT_LOG_ID, $formSubmitLogId, STORE_EXTRA);

    }

    /**
     * Handles upload protection enforcement based on system configuration.
     *
     * This method:
     * - Retrieves the configured protection handling strategy
     * - Checks if the uploaded File requires Adobe Reader (e.g. Portfolio or XFA)
     * - Checks if the file is encrypted and optionally attempts to remove protection
     * - Enforces denial rules based on flags like `UN_PROTECT`, `DENY_PROTECT`, and `DENY_ACROBAT`
     *
     * Possible actions:
     * - Tries to unprotect the File using Ghostscript
     * - Throws a 422 error if the file cannot be decrypted and denial is configured
     * - Throws a 422 error for Adobe-only Files if configured to reject them
     * - Throws a 500 error for unexpected runtime failures
     *
     * @param $tmpFile
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function handleUploadProtection($tmpFile): void {

        $pdfActionRaw = $this->store->getVar(SYSTEM_UPLOAD_ACTION, STORE_SIP . STORE_SYSTEM);

        // Default fallback actions if config is missing
        if ($pdfActionRaw === false || $pdfActionRaw === '') {
            $pdfActionRaw = SYSTEM_UPLOAD_ACTION_UN_PROTECT . ',' . SYSTEM_UPLOAD_ACTION_DENY_PROTECT;
        }

        $pdfActions = KeyValueStringParser::explodeEscape(',', $pdfActionRaw);

        // Validate that 'none' is not combined with other actions
        if (in_array(SYSTEM_UPLOAD_ACTION_NONE, $pdfActions) && count($pdfActions) > 1) {
            throw new \UserFormException('Parameter \'uploadAction\': "none" cannot be combined with other actions.');
        }

        $timeoutUnprotect = 20;
        $new = array();
        // Iterate all given keywords. Trim them. 'unProtect' might be specified as 'unProtect:60' - than the max timeout to unprotext is 60s.
        foreach ($pdfActions as $value) {
            $arr = explode(':', $value);
            $new[] = trim($arr[0]);
            if ( $arr[0] == SYSTEM_UPLOAD_ACTION_UN_PROTECT && is_numeric($arr[1]??'')) {
                $timeoutUnprotect = $arr[1];
            }
        }
        $pdfActions=$new;

        $hasDenyProtect = in_array(SYSTEM_UPLOAD_ACTION_DENY_PROTECT, $pdfActions);
        $hasUnprotect = in_array(SYSTEM_UPLOAD_ACTION_UN_PROTECT, $pdfActions);

        // Deny Adobe-only PDFs (e.g., portfolios) if configured
        if (in_array(SYSTEM_UPLOAD_ACTION_DENY_ACROBAT, $pdfActions)) {
            try {
                if (HelperFile::checkPdfAcrobat($tmpFile)) {
                    throw new \UserFormException('It seems the PDF requires Adobe Acrobat only — please convert file to not rely on Adobe only.');
                }
            } catch (\RuntimeException $e) {
                header('HTTP/1.1 500 Internal Server Error');
                throw $e;
            } catch (\UserFormException $e) {
                header('HTTP/1.1 422 Unprocessable Entity');
                throw $e;
            }
        }

        // Try to unprotect the file
        if ($hasUnprotect && HelperFile::isPdfProtected($tmpFile)) {
            try {
                HelperFile::removePdfProtection($tmpFile, $timeoutUnprotect);
            } catch (\UserFormException $e) {
                // If denyProtect is also active, fail upload
                if ($hasDenyProtect) {
                    header('HTTP/1.1 422 Unprocessable Entity');
                    throw $e;
                }
                // Else ignore (fail silently if only unprotect is set)
            } catch (\RuntimeException $e) {
                header('HTTP/1.1 500 Internal Server Error');
                throw $e;
            }
        }

        if ($hasDenyProtect) {
            try {
                // Check if the PDF is (still) protected
                $isProtected = HelperFile::isPdfProtected($tmpFile);
            } catch (\UserFormException $e) {
                // If denyProtect is also active, fail upload
                if ($hasDenyProtect) {
                    header('HTTP/1.1 422 Unprocessable Entity');
                    throw new $e;
                }
                // Else ignore
                $isProtected = false;
            } catch (\RuntimeException $e) {
                header('HTTP/1.1 500 Internal Server Error');
                throw $e;
            }
        }

        // Deny upload if still protected and deny is enabled
        if ($isProtected && $hasDenyProtect) {
            header('HTTP/1.1 422 Unprocessable Entity');
            throw new \UserFormException('Uploaded PDF is protected.');
        }
    }


    /**
     *  Validates the dimension of an uploaded image file (either via traditional upload or FilePond).
     *
     *  This function checks if an uploaded file is an image, and if so, verifies that its dimensions
     *  do not exceed the maximum width and height defined in the system configuration (from SIP or STORE_SYSTEM).
     *
     *  If the image is invalid (e.g., unreadable or too large), a `413 Payload Too Large` response header is sent,
     *  and a `CodeException` is thrown. This allows frontend to handle the error appropriately.
     *
     * @param $tmpFile
     * @param $fileType
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function checkMaxImageDimension($tmpFile, $fileType): void {
        $filePath = '';

        if (!str_starts_with($fileType, 'image/') || !file_exists($tmpFile)) {
            return;
        }

        $dimensions = getimagesize($tmpFile);
        if ($dimensions === false) {
            // Add "413 Content Too Large" so filePond can respond correctly
            header('HTTP/1.0 ' . 413);
            throw new \CodeException("Invalid image file.", 413);
        }
        // Get Dimensions out of the SIP or Store
        // SIP is set in UploadFormElement.php
        $maxDimensions = explode('x', $this->store->getVar(SYSTEM_MAX_IMAGE_DIMENSION, STORE_SIP . STORE_SYSTEM));
        $maxWidth = (int)$maxDimensions[0];
        $maxHeight = (int)$maxDimensions[1];

        if ($dimensions[0] > $maxWidth || $dimensions[1] > $maxHeight) {
            // Add "413 Content Too Large" so filePond can respond correctly
            header('HTTP/1.0 ' . 413);
            throw new \CodeException("Image size is too big. Max Size {$maxWidth}x{$maxHeight}, Uploaded Size {$dimensions[0]}x{$dimensions[1]}", 413);
        }
    }
}