<?php
/**
 * Manage main actions
 *
 * @category PHP
 * @package  VenoFileManager
 * @author   Nicola Franchini <info@veno.it>
 * @license  https://veno.es/shop/help Veno Commercial License
 * @link     http://filemanager.veno.it/
 */
if (!class_exists('Actions', false)) {
    /**
     * Class Actions
     *
     * @category PHP
     * @package  VenoFileManager
     * @author   Nicola Franchini <info@veno.it>
     * @license  https://veno.es/shop/help Veno Commercial License
     * @link     http://filemanager.veno.it/
     */
    class Actions
    {
        /**
         * The main function, checks if the user wants to perform any supported operations
         *
         * @param string $location to run
         *
         * @return checks if any action is required
         */
        public function __construct($location = false)
        {
            if (!$location) {
                return false;
            }

            global $gateKeeper;

            $postnewdir = filter_input(INPUT_POST, 'userdir', FILTER_SANITIZE_SPECIAL_CHARS);
            $renamedir = filter_input(INPUT_POST, 'newname', FILTER_SANITIZE_SPECIAL_CHARS);
            $postoldname = filter_input(INPUT_POST, 'oldname', FILTER_SANITIZE_SPECIAL_CHARS);
            $getdel = filter_input(INPUT_GET, 'del', FILTER_SANITIZE_SPECIAL_CHARS);

            // delete files or folders
            if ($getdel && $gateKeeper->isAllowed('delete_enable')) {
                $decode_del = base64_decode($getdel, true);

                if ($decode_del) {
                    $getdel = Utils::extraChars(urldecode($decode_del));
                    $this->setDel($getdel);
                } else {
                    Utils::setError('<i class="bi bi-slash-circle"></i> Character encoding error, remove the file via FTP');
                }
            }

            // add new folder
            if ($postnewdir) {
                $dirname = Utils::normalizeStr($postnewdir);
                Actions::newFolder($location, $dirname);
            }
            // rename files or folders
            if ($renamedir && $postoldname) {
                $postoldname = base64_decode($postoldname, true); // Check also dir
                $this->setRename($postoldname, Utils::normalizeStr($renamedir));
            }
            // Upload files in the old fashioned way ( IE <= 9 & Safari on Windows)
            if (isset($_FILES['userfile']['name'])) {
                Uploader::uploadMulti($_FILES['userfile']);
            }
        }

        /**
         * Check path to delete
         *
         * @param string $path to search
         *
         * @return true/false
         */
        public static function checkDel($path)
        {
            global $gateKeeper;
            global $setUp;

            $startdir = $setUp->getConfig('starting_dir');

            $cash = filter_input(INPUT_GET, 'h', FILTER_SANITIZE_SPECIAL_CHARS);
            $del = filter_input(INPUT_GET, 'del', FILTER_SANITIZE_SPECIAL_CHARS);

            $del = str_replace(' ', '+', $del);

            if (md5($del.$setUp->getConfig('salt').$setUp->getConfig('session_name')) === $cash) {

                if ($gateKeeper->getUserInfo('dir') != null) {
                    $userdirs = json_decode($gateKeeper->getUserInfo('dir'), true);

                    foreach ($userdirs as $value) {
                        $userpath = $startdir.$value;
                        $pos = strpos('./'.$path, $userpath);

                        if ($pos !== false) {
                            return true;
                        }
                    }
                    return false;
                }
                $pos = strpos('./'.$path, $startdir);

                $filepathinfo = Utils::mbPathinfo($path);
                $basepath = $filepathinfo['basename'];
                $evil = array('', '/', '\\', '.');
                $avoid = $setUp->getConfig('hidden_files');

                if ($pos === false
                    || in_array($path, $evil)
                    || in_array($basepath, $avoid)
                    || realpath($path) === realpath($startdir)
                    || realpath($path) === realpath(dirname(__FILE__))
                ) {
                    return false;
                }
                return true;
            }
            return false;
        }

        /**
         * Setup file to delete
         *
         * @param string $getdel path to delete
         *
         * @return call deleteFile() or deleteDir()
         */
        public function setDel($getdel)
        {
            global $gateKeeper;
            global $setUp;

            if (Actions::checkDel($getdel) == false) {
                Utils::setError('<i class="bi bi-slash-circle"></i> '.$setUp->getString('not_allowed'));
                return;
            }
            if (is_dir($getdel)) {
                if (Utils::isDemoProtected($getdel)) {
                    Utils::setError('This folder cannot be deleted in demo mode');
                    return;
                }
                if ($gateKeeper->getUserSpace() !== false) {
                    $ritorno = Utils::getDirSize("./".$getdel);
                    $totalsize = $ritorno['size'];

                    if ($totalsize > 0) {
                        Actions::updateUserSpace(false, true, $totalsize);
                    }
                }
                Actions::deleteDir($getdel);

                Utils::setWarning('<span><i class="bi bi-trash"></i> <strong>'.substr($getdel, strrpos($getdel, '/') + 1).'</strong></span>');
                // Directory successfully deleted, sending log notification
                Logger::logDeletion('./'.$getdel, true);

            } elseif (is_file($getdel)) {
                Actions::deleteFile($getdel);
            }
        }

        /**
         * Delete directory
         *
         * @param string $dir directory to delete
         *
         * @return deletes directory
         */
        public static function deleteDir($dir)
        {
            if (is_dir($dir)) {
                $objects = scandir($dir);
                foreach ($objects as $object) {
                    if ($object != '.' && $object != '..') {
                        if (filetype($dir.'/'.$object) == 'dir') {
                            Actions::deleteDir($dir.'/'.$object);
                        } else {
                            $filetime = filemtime($dir.'/'.$object);
                            $filetime = $filetime ? $filetime : 'no-data';
                            unlink($dir.'/'.$object);
                            Actions::deleteThumb($dir.'/'.$object, $filetime);
                        }
                    }
                }
                reset($objects);
                rmdir($dir);
            }
        }

        /**
         * Delete file
         *
         * @param string $file  file to delete
         * @param bool   $multi delete multiple files
         *
         * @return deletes file
         */
        public static function deleteFile($file, $multi = false)
        {
            if (is_file($file)) {

                Actions::updateUserSpace($file, true);

                $filepathinfo = Utils::mbPathinfo($file);
                $basepath = $filepathinfo['basename'];

                $filetime = filemtime($file);
                $filetime = $filetime ? $filetime : 'no-data';

                if ($multi) {
                    Actions::deleteThumb(substr($file, 6), $filetime, true);
                    Utils::setWarning('<span><i class="bi bi-trash"></i> <strong>'.$basepath.'</strong></span> ');
                    Logger::logDeletion(substr($file, 1), false, true);
                } else {
                    $filetime = filemtime($file);
                    $filetime = $filetime ? $filetime : 'no-data';
                    Actions::deleteThumb($file, $filetime);
                    Utils::setWarning('<span><i class="bi bi-trash"></i> <strong>'.$basepath.'</strong></span>');
                    Logger::logDeletion('./'.$file, false);
                }
                unlink($file);
            }
        }

        /**
         * Setup file renaming
         *
         * @param string $postoldname original file or directory name
         * @param string $postnewname new file or directory name
         *
         * @return call renameFile();
         */
        public function setRename($postoldname, $postnewname)
        {
            global $gateKeeper;
            global $setUp;

            if ($gateKeeper->isAccessAllowed() && $gateKeeper->isAllowed('rename_enable')) {

                $postthisext = htmlspecialchars($_POST['thisext']);
                $postthisdir = filter_input(INPUT_POST, 'thisdir', FILTER_SANITIZE_SPECIAL_CHARS);

                // --- VALIDAZIONE DIRECTORY ---
                $baseDirConfig = $setUp->getConfig('starting_dir');
                $rawBaseDir = realpath($baseDirConfig);
                if (!$rawBaseDir) {
                    Utils::setError('Configuration error: Upload directory not accessible.');
                    return;
                }
                $realBaseDir = str_replace('\\', '/', $rawBaseDir);
                $realBaseDir = rtrim($realBaseDir, '/') . '/';

                // Directory consentite
                $userDirsJson = $gateKeeper->getUserInfo('dir');
                $userRelativeAllowedDirs = $userDirsJson ? json_decode($userDirsJson, true) : [];
                $effectiveAbsoluteAllowedDirs = [];

                if (!empty($userRelativeAllowedDirs) && is_array($userRelativeAllowedDirs)) {
                    foreach ($userRelativeAllowedDirs as $relativeDir) {
                        $fullPath = realpath($rawBaseDir . DIRECTORY_SEPARATOR . $relativeDir);
                        if ($fullPath) {
                            $norm = str_replace('\\', '/', $fullPath);
                            $effectiveAbsoluteAllowedDirs[] = rtrim($norm, '/') . '/';
                        }
                    }
                } else {
                    $effectiveAbsoluteAllowedDirs[] = $realBaseDir;
                }

                // --- DETERMINA IL TARGET ---
                $baseDirName = basename($realBaseDir);
                $userProvidedPath = str_replace('\\', '/', $postthisdir);

                // Pulisce il prefisso ridondante (es: 'uploads/')
                $relativeCleaning = $userProvidedPath;
                if (stripos($userProvidedPath, $baseDirName . '/') === 0) {
                    $relativeCleaning = substr($userProvidedPath, strlen($baseDirName . '/'));
                } elseif (strcasecmp($userProvidedPath, $baseDirName) === 0) {
                    $relativeCleaning = '';
                }

                // Usa ltrim per evitare doppi slash nella concatenazione
                $potentialTargetPath = $realBaseDir . ltrim($relativeCleaning, '/');
                $sanitizedTargetDir = Utils::normalizePath($potentialTargetPath);
                $sanitizedTargetDir = rtrim($sanitizedTargetDir, '/') . '/';

                // --- CONFRONTO FINALE ---
                $isTargetDirValid = false;
                foreach ($effectiveAbsoluteAllowedDirs as $allowedDir) {
                    if (stripos($sanitizedTargetDir, $allowedDir) === 0) {
                        $isTargetDirValid = true;
                        break;
                    }
                }

                if (!$isTargetDirValid) {
                    Utils::setError('<i class="bi bi-slash-circle"></i> '.$setUp->getString('not_allowed'));
                    return;
                }

                // --- ESECUZIONE ---
                if ($postoldname && $postnewname) {
                    $fileExtension = $postthisext ? '.' . $postthisext : '';
                    $oldname_filename_part = $postoldname . $fileExtension;
                    $newname_filename_part = Utils::normalizeStr($postnewname) . $fileExtension;

                    // Costruiamo i percorsi finali (normalizePath toglierà lo slash finale se è un file)
                    $oldname = Utils::normalizePath($sanitizedTargetDir . $oldname_filename_part);
                    $newname = Utils::normalizePath($sanitizedTargetDir . $newname_filename_part);

                    if (Utils::isDemoProtected($oldname)) {
                        Utils::setError('This folder cannot be renamed in demo mode');
                        return;
                    }

                    Actions::renameFile($oldname, $newname, $postnewname);
                }
            }
        }

        /**
         * Rename files
         *
         * @param string $oldname    original file path
         * @param string $newname    new file path
         * @param string $thenewname new name
         * @param bool   $move       move file
         * @param bool   $copy       copy file
         *
         * @return boolean
         */
        public static function renameFile($oldname, $newname, $thenewname, $move = false, $copy = false)
        {
            global $setUp;
            global $gateKeeper;

            static $allowedDirsCache = null;

            $newname = Utils::extraChars($newname);
            
            if (!Utils::fileExists($oldname)) {
                Utils::setError('<span><i class="bi bi-exclamation-circle"></i> <strong>' .$oldname. '</strong> does not exist</span>');
                return false;
            }

            if (Utils::isDemoProtected($oldname)) {
                Utils::setError('This item cannot be modified in demo mode');
                return false;
            }

            if (Utils::fileExists($newname)) {
                Utils::setWarning('<span><i class="bi bi-info-circle"></i> <strong>' .$thenewname. '</strong> '.$setUp->getString('file_exists').'</span>');
                return false;
            }

            // --- LOGICA DI VALIDAZIONE OTTIMIZZATA CON CACHE ---
            if ($allowedDirsCache === null) {
                $allowedDirsCache = [];
                $startingDirConfig = $setUp->getConfig('starting_dir');
                $projectRoot = dirname(dirname(__DIR__));
                $rawBaseDir = realpath($projectRoot . DIRECTORY_SEPARATOR . ltrim($startingDirConfig, './'));
                
                if ($rawBaseDir) {
                    $realBaseDir = str_replace('\\', '/', $rawBaseDir);
                    $realBaseDir = rtrim($realBaseDir, '/') . '/';

                    $userDirsJson = $gateKeeper->getUserInfo('dir');
                    $userRelativeAllowedDirs = $userDirsJson ? json_decode($userDirsJson, true) : [];
                    
                    if (!empty($userRelativeAllowedDirs) && is_array($userRelativeAllowedDirs)) {
                        foreach ($userRelativeAllowedDirs as $relativeDir) {
                            $fullPath = realpath($realBaseDir . $relativeDir);
                            if ($fullPath) {
                                $normalized = str_replace('\\', '/', $fullPath);
                                $allowedDirsCache[] = rtrim($normalized, '/') . '/';
                            }
                        }
                    } else {
                        $allowedDirsCache[] = $realBaseDir;
                    }
                }
            }

            if (empty($allowedDirsCache)) {
                return false;
            }

            // Standardizziamo i percorsi da controllare
            $realOldPath = realpath($oldname);
            $realNewDirPath = realpath(dirname($newname));

            if (!$realOldPath || !$realNewDirPath) {
                Utils::setError('Invalid path: target or destination directory not found.');
                return false;
            }

            $realOldPath = str_replace('\\', '/', $realOldPath);
            $realNewDirPath = str_replace('\\', '/', $realNewDirPath);
            $realNewDirPath = rtrim($realNewDirPath, '/') . '/';

            // Validazione Sorgente e Destinazione
            $isOldValid = false;
            $isNewValid = false;

            foreach ($allowedDirsCache as $allowedDir) {
                // Confronto case-insensitive per massima compatibilità (es. Windows/MAMP)
                if (stripos($realOldPath, $allowedDir) === 0) {
                    $isOldValid = true;
                }
                if (stripos($realNewDirPath, $allowedDir) === 0) {
                    $isNewValid = true;
                }
                if ($isOldValid && $isNewValid) break;
            }

            if (!$isOldValid || !$isNewValid) {
                Utils::setError('Invalid path for rename operation. Access denied to target or destination.');
                return false;
            }
            // --- FINE LOGICA DI VALIDAZIONE ---

            if ($copy) {
                if (!copy($oldname, $newname)) {
                    Utils::setError('<span><i class="bi bi-exclamation-circle"></i> <strong>' .$thenewname. '</strong> can\'t be copied</span>');
                    return false;
                }
                Actions::updateUserSpace($oldname);
                return true;
            } else {
                $filetime = filemtime($oldname) ?: 'no-data';
                
                if (is_dir($oldname)) {
                    Actions::deleteThumbRecursive($oldname);
                }

                if (!rename($oldname, $newname)) {
                    Utils::setError('<span><i class="bi bi-exclamation-circle"></i> <strong>' .$thenewname. '</strong> can\'t be edited</span>');
                    return false;
                }

                if ($move) {
                    $delfilename = substr($oldname, strlen(dirname(dirname(__DIR__))) + 1);
                    Actions::deleteThumb($delfilename, $filetime, true);
                } else {
                    Utils::setSuccess('<span><strong>'.$thenewname. '</strong> '.$setUp->getString('updated').'</span>');
                    Actions::deleteThumb($oldname, $filetime);
                }
                return true;
            }
        }

        /**
         * Show available directories
         *
         * @param string $dir        base path
         * @param string $currentdir current directory browsing
         * @param bool   $link       set link or data-dest
         * @param string $relative   relative path '../.' to be called from /vfm-admin/ajax/ instead of '' from index
         * @param bool   $checkbox   checkbox
         * @param array  $userdirs   user dirs
         * @param int    $level      level
         *
         * @return directories tree
         */
        public static function walkDir($dir, $currentdir, $link = false, $relative = '', $checkbox = false, $userdirs = array(), $level = 0)
        {
            global $setUp;
            $counter = 0;
            $output = false;

            $relativedir = $relative.$dir;

            if (is_dir($relativedir)) {
                $scandir = glob($relativedir.'*', GLOB_ONLYDIR);

                natcasesort($scandir);

                $cleandir = ltrim($dir, $setUp->getConfig('starting_dir'));
                $level++;
                $ulclass = $level > 1 ? ' class="sub-tree"' : '';
                $output = '<ul'.$ulclass.'>';

                foreach ($scandir as $folder) {

                    $filepathinfo = Utils::mbPathinfo($folder);
                    $file = $filepathinfo['basename'];

                    if ($relativedir.$file == $relative.'./vfm-admin' || substr($file, 0, 1) == '.') {
                        continue;
                    }

                    if (is_dir($relativedir.$file)) {

                        $counter++;

                        $output .= '<li class="level-'.$level.' level-'.$level.'-'.$counter.'">';

                        if ($level == 1) {
                            $output .= '<div class="highlight"></div>';
                        }

                        $dest = ltrim($dir.$file, './');
                        $cleandest = ltrim($cleandir.$file, './');
                        $checked = is_array($userdirs) && in_array($cleandest, $userdirs) ? ' checked' : '';

                        if ($checkbox) {
                            $box_id = md5($dest);
                            $output .= '<div class="form-check">
                              <input class="form-check-input setuserfolder" style="vertical-align: sub; float:none;" type="checkbox" value="'.$cleandest.'" id="'.$box_id.'"'.$checked.' data-target=".level-'.$level.'-'.$counter.'">
                              <label class="form-check-label" for="'.$box_id.'">'.$file.'</label>
                            </div>';
                        } else {
                            if ($relativedir.$file."/" == $relative.$currentdir) {
                                $output .= '<i class="bi bi-folder2-open"></i> <span class="search-highlight">'.$file.'</span>';
                            } else {
                                if ($link) {
                                    $output .= '<a data href="?dir='.urlencode($dest).'">';
                                } else {
                                    $output .= '<a href="#" data-dest="'.urlencode('./'.$dest).'" class="movelink">';
                                }
                                $output .= '<i class="bi bi-folder"></i> '.$file.'</a>';
                            }
                        }

                        // Break loop for checkboxes > 8 && foldertee > 30 levels
                        if (($checkbox && $level < 8) || (!$checkbox && $level < 30)) {
                            $scansub = glob($relativedir.$file.'/'.'*', GLOB_ONLYDIR);
                            if (count($scansub)) {
                                $output .= '<span class="toggle-tree"><i class="bi bi-dash-square tree-toggler bg-lighter"></i></span>';
                                $output .= Actions::walkDir($relativedir.$file.'/', $relative.$currentdir, $link, '', $checkbox, $userdirs, $level);
                            }
                        }
                        $output .= '</li>';
                    }
                }

                $output .= '</ul>';
            }
            return $output;
        }

        /**
         * Create new folder
         *
         * @param string $location where to create new folder
         * @param string $dirname  new dir name
         *
         * @return adds new folder
         */
        public static function newFolder($location, $dirname)
        {
            global $setUp;
            global $gateKeeper;

            if ($gateKeeper->isAllowed('newdir_enable')) {
                if (strlen($dirname) > 0) {
                    if (!$location->editAllowed()) {
                        // The system configuration does not allow uploading here
                        Utils::setError($setUp->getString('upload_not_allowed'));
                        return false;
                    }
                    if (!$location->isWritable()) {
                        // The target directory is not writable
                        Utils::setError($setUp->getString('upload_dir_not_writable'));
                        return false;
                    }
                    if (Utils::fileExists($location->getDir(true, false, false, 0).$dirname)) {
                        Utils::setWarning(
                            '<span><i class="bi bi-folder"></i> <strong>'.$dirname.'</strong> '
                            .$setUp->getString('file_exists').'</span>'
                        );
                        return false;
                    }
                    if (!mkdir($location->getDir(true, false, false, 0).$dirname, 0755)) {
                        // Error creating a new directory
                        Utils::setError($setUp->getString('new_dir_failed'). ': <strong>'.$dirname.'</strong>');
                        return false;
                    }
                    Utils::setSuccess(
                        '<span><i class="bi bi-folder"></i> <strong>'.$dirname.'</strong> '
                        .$setUp->getString('created').'</span>'
                    );
                    // Directory successfully created, sending e-mail notification
                    Logger::logCreation($location->getDir(true, false, false, 0).$dirname, true);
// Redirect to the new folder.
// header('Location:'.$setUp->getConfig('script_url')."?dir=".$location->getDir(false, false, false, 0).$dirname);
                    return true;
                }
                Utils::setError($setUp->getString('new_dir_failed'). ': <strong>'.$dirname.'</strong>');
                return false;
            }
        }


        /**
         * Delete directory
         *
         * @param string $dir directory to delete
         *
         * @return deletes directory
         */
        public static function deleteThumbRecursive($dir)
        {
            if (is_dir($dir)) {
                $objects = scandir($dir);
                foreach ($objects as $object) {
                    if ($object != '.' && $object != '..') {
                        if (filetype($dir.'/'.$object) == 'dir') {
                            Actions::deleteThumbRecursive($dir.'/'.$object);
                        } else {
                            $filetime = filemtime($dir.'/'.$object);
                            $filetime = $filetime ? $filetime : 'no-data';
                            Actions::deleteThumb($dir.'/'.$object, $filetime);
                        }
                    }
                }
                reset($objects);
            }
        }

        /**
         * Delete thumbnail
         *
         * @param string $file     file to delete
         * @param string $filetime file time
         * @param bool   $multi    called from vfm-del.php or vfm-move.php
         *
         * @return deletes thumb
         */
        public static function deleteThumb($file, $filetime = 'no-data', $multi = false)
        {
            if ($multi == false) {
                $relativepath = 'vfm-admin/';
            } else {
                $relativepath = '../';
            }
            $thumbdir = $relativepath.'_content/thumbs/';

            $thumbname = md5($file.$filetime);

            $thumb = $thumbdir.$thumbname.'.jpg';
            $thumb_big = $thumbdir.$thumbname.'-big.jpg';

            if (is_file($thumb)) {
                unlink($thumb);
            }
            if (is_file($thumb_big)) {
                unlink($thumb_big);
            }
        }

        /**
         * Check if user has space to upload
         *
         * @param string $file     file to check
         * @param int    $thissize size to check
         * @param bool   $upload   uploaded file
         *
         * @return true/false
         */
        public static function checkUserSpace($file = false, $thissize = false, $upload = false)
        {
            global $setUp;

            if ($upload) {
                $max_upload_filesize = $setUp->getConfig('max_upload_filesize');
                if ($max_upload_filesize > 0) {
                    $max_upload_filesize = ($max_upload_filesize*1024*1024);
                    if (!$thissize && $file) {
                        $thissize = Utils::getFileSize($file);
                    }
                    if ($thissize > $max_upload_filesize) {
                        return false;
                    }
                }
            }

            if (isset($_SESSION['vfm_user_used'])
                && isset($_SESSION['vfm_user_space'])
            ) {
                if (!$thissize && $file) {
                    $thissize = Utils::getFileSize($file);
                }
                $oldused = $_SESSION['vfm_user_used'];
                $newused = $oldused + $thissize;
                $freespace = $_SESSION['vfm_user_space'];
                
                if ($newused > $freespace) {
                    return false;
                }
            }
            return true;
        }

        /**
         * Update user used space by file (add or subtract)
         *
         * @param string  $file file to add/subtract
         * @param boolean $add  true/false add or subtract
         * @param int     $size size to add/subtract
         *
         * @return updates total used space
         */
        public static function updateUserSpace($file = false, $add = false, $size = false)
        {
            if (isset($_SESSION['vfm_user_used'])) {

                $thissize = ($size && !$file) ? $size : Utils::getFileSize($file);
                $usedspace = $_SESSION['vfm_user_used'];

                if ($add) {
                    $usedspace = (abs($usedspace) - abs($thissize));
                } else {
                    $usedspace = (abs($usedspace) + abs($thissize));
                }
                $_SESSION['vfm_user_used'] = $usedspace;
            }
        }
    }
}
