<?php
/**
 * Stream videos
 *
 * @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('VideoStream', false)) {
    /**
     * VideoStream class
     *
     * PHP version >= 5.3
     *
     * @category PHP
     * @package  VenoFileManager
     * @author   Nicola Franchini <info@veno.it>
     * @license  https://veno.es/shop/help Veno Commercial License
     * @link     http://filemanager.veno.it/ - https://gist.github.com/ranacseruet/9826293
     */
    class VideoStream
    {
        private $_path = "";
        private $_stream = "";
        private $_buffer = 8 * 1024;
        private $_start  = -1;
        private $_end    = -1;
        private $_size   = 0;
        private $_videoFormats = array(
            "mp4"=>"video/mp4",
            "webm"=>"video/webm",
            "ogg"=>"video/ogg",
            "ogv"=>"video/ogg",
           // "flv"=>"video/x-flv",
        );
        /**
         * Construct.
         *
         * @param string $filePath file path
         */
        function __construct($filePath)
        {
            $decodedPath = base64_decode($filePath);
            if ($decodedPath === false) {
                // Errore nella decodifica base64, interrompi.
                die('Invalid file identifier.');
            }
            // Rimuoviamo urldecode e puliamo il percorso da eventuali tentativi di traversal iniziali
            $decodedPath = urldecode($decodedPath);
            $decodedPath = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $decodedPath);
            $cleanPath = str_replace('..' . DIRECTORY_SEPARATOR, '', $decodedPath);
            
            // Costruiamo il percorso assoluto della root dell'app
            $appRoot = dirname(dirname(__DIR__));
            $this->_path = $appRoot . DIRECTORY_SEPARATOR . $cleanPath;
        }


        /**
         * Controlla l'accesso e la validità del file.
         * Verifica che il file esista, sia all'interno della directory consentita (starting_dir)
         * e appartenga all'utente corrente (se applicabile).
         *
         * @return true se l'accesso è consentito, altrimenti false o genera un errore.
         */
        public function checkAccess()
        {
            global $setUp, $gateKeeper; // Assicurati che $gateKeeper sia disponibile se necessario

            // Verifica se l'utente è autenticato e ha il permesso di streaming video
            // Questo controllo è già presente in streamvid.php, ma una doppia verifica qui non guasta.
            if (!$gateKeeper->isAccessAllowed() && $setUp->getConfig('share_playvideo') !== true) {
                http_response_code(403); // Forbidden
                die('Access denied: User not authenticated or permission missing.');
            }

            $appRoot = realpath(dirname(dirname(__DIR__)));
            $realFilePath = realpath($this->_path);
            
            // Costruiamo il percorso di partenza usando il separatore di sistema
            $startingDirConfig = $setUp->getConfig('starting_dir');
            $fullStartingPath = $appRoot . DIRECTORY_SEPARATOR . ltrim($startingDirConfig, './');
            $realStartingDir = realpath($fullStartingPath);

            if ($realFilePath === false || $realStartingDir === false) {
                http_response_code(404);
                die('File not found.');
            }

            // Ora strpos è sicuro su Windows perché realpath() ha uniformato i \
            if (strpos($realFilePath, $realStartingDir) !== 0) {
                http_response_code(403);
                die('Access denied: Outside sandbox.');
            }

            $forbiddenPaths = array(
                realpath($appRoot . '/vfm-admin'),
            );

            foreach ($forbiddenPaths as $forbidden) {
                if ($forbidden && strpos($realFilePath, $forbidden) === 0) {
                    http_response_code(403);
                    die('Access denied: System folder protection.');
                }
            }
            // 3. Validazione del tipo di file.
            $fileExtension = strtolower(pathinfo($this->_path, PATHINFO_EXTENSION));
            if (!array_key_exists($fileExtension, $this->_videoFormats)) {
                http_response_code(400); // Bad Request
                die('Unsupported file format. Only video formats are allowed.');
            }

            // Se tutti i controlli passano
            return true;
        }

        /**
         * Open stream
         *
         * @return fopen stream
         */
        private function _open()
        {
            if (!($this->_stream = fopen($this->_path, 'rb'))) {
                http_response_code(500); // Internal Server Error
                die('Could not open stream for reading');
            }
        }
         
        /**
         * Set proper header to serve the video content.
         *
         * @return video headers
         */
        private function _setHeader()
        {
            ob_get_clean();

            $fileExtension = strtolower(pathinfo($this->_path, PATHINFO_EXTENSION));

            header("Content-Type: " . $this->_videoFormats[$fileExtension]);
            header("Cache-Control: max-age=2592000, public");
            header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
            header("Last-Modified: ".gmdate('D, d M Y H:i:s', @filemtime($this->_path)) . ' GMT');
            
            $this->_start = 0;
            $this->_size  = filesize($this->_path);
            $this->_end   = $this->_size - 1;
            
            header("Accept-Ranges: 0-".$this->_end);
             
            if (isset($_SERVER['HTTP_RANGE'])) {
                $c_start = $this->_start;
                $c_end = $this->_end;
     
                list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
                if (strpos($range, ',') !== false) {
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
                    header("Content-Range: bytes $this->_start-$this->_end/$this->_size");
                    exit;
                }
                if ($range == '-') {
                    $c_start = $this->_size - substr($range, 1);
                } else {
                    $range = explode('-', $range);
                    $c_start = $range[0];
                     
                    $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
                }
                $c_end = ($c_end > $this->_end) ? $this->_end : $c_end;
                if ($c_start > $c_end || $c_start > $this->_size - 1 || $c_end >= $this->_size) {
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
                    header("Content-Range: bytes $this->_start-$this->_end/$this->_size");
                    exit;
                }
                $this->_start = $c_start;
                $this->_end = $c_end;
                $length = $this->_end - $this->_start + 1;
                fseek($this->_stream, $this->_start);
                header('HTTP/1.1 206 Partial Content');
                header("Content-Length: ".$length);
                header("Content-Range: bytes $this->_start-$this->_end/".$this->_size);
            } else {
                header("Content-Length: ".$this->_size);
            }
        }

        /**
         * Close curretly opened stream
         *
         * @return fclose stream
         */
        private function _end()
        {
            if ($this->_stream && is_resource($this->_stream)) {
                fclose($this->_stream);
            }
            exit;
        }
         
        /**
         * Perform the streaming of calculated range
         *
         * @return stream
         */
        private function _stream()
        {
            // checkAccess() è stato già chiamato prima di arrivare qui, quindi il file è valido e accessibile.
            $i = $this->_start;
            set_time_limit(0); // Disabilita il timeout per lo script, necessario per file grandi.

            while (!feof($this->_stream) && $i <= $this->_end) {
                $bytesToRead = $this->_buffer;
                // Assicura di non leggere oltre la fine del range richiesto.
                if (($i + $bytesToRead) > $this->_end) {
                    $bytesToRead = $this->_end - $i + 1;
                }
                $data = fread($this->_stream, $bytesToRead);
                if ($data === false) {
                    // Errore nella lettura del file.
                    http_response_code(500); // Internal Server Error
                    error_log("Error reading video stream from: " . $this->_path);
                    break; // Esci dal loop se c'è un errore di lettura.
                }
                echo $data;
                flush(); // Invia i dati immediatamente al browser.
                $i += $bytesToRead;
            }
        }
         
        /**
         * Start streaming video content
         *
         * @return start stream
         */
        function _start()
        {
            if ($this->checkAccess()) {
                $this->_open();       // Apri il file.
                $this->_setHeader();  // Imposta gli header HTTP.
                $this->_stream();     // Esegui lo streaming dei dati.
            }
            $this->_end();            // Chiudi lo stream ed esci (anche se checkAccess fallisce).
        }
    }
}
