<?php
/**
 * Script Updater
 *
 * @category PHP
 * @package  VenoFileManager
 * @author   Nicola Franchini <support@veno.it>
 * @license  https://veno.es/shop/help Veno Commercial License
 * @link     http://filemanager.veno.it/
 */
if (!class_exists('VFMUpdater', false)) {
    /**
     * Plugin Updater class
     *
     * @category PHP
     * @package  VenoFileManager
     * @author   Nicola Franchini <support@veno.it>
     * @license  https://veno.es/shop/help Veno Commercial License
     * @link     http://veno.es
     */
    class VFMUpdater
    {
        /**
         * Plugin slug
         *
         * @var _slug
         */
        private $_slug = 'vfm';

        /**
         * Package name
         *
         * @var _package_zip
         */
        private $_package_zip = 'vfm_package.zip';

        /**
         * Package name
         *
         * @var _upgrade_dir
         */
        private $_upgrade_dir = false;

        /**
         * Package name
         *
         * @var _upgrade_dir
         */
        private $_upgrade_folder = 'UPGRADE';

        /**
         * Package name
         *
         * @var _main_dir
         */
        private $_main_dir = false;

        /**
         * Log
         *
         * @var _log
         */
        private $_log = array();

        /**
         * Check for updates
         *
         * @var _update_url
         */
        public $_update_url = 'https://veno.es/updates/';

        /**
         * Holds an instance of the object
         *
         * @var $instance
         */
        protected static $instance = null;

        /**
         * Returns the running object
         *
         * @return VFMUpdater
         */
        public static function getInstance()
        {
            if (null === self::$instance) {
                self::$instance = new self();
            }
            return self::$instance;
        }

        /**
         * Start up
         */
        public function __construct()
        {
            $this->_main_dir = dirname(dirname(dirname(__DIR__)));
            $this->_upgrade_dir = $this->_main_dir.'/'.$this->_upgrade_folder;
        }

        /**
         * Get package info
         *
         * @return downloaded package path
         */
        public function checkUpdates()
        {
            global $setUp;
            $getinfo = $this->getInfo();
            $response = array(
                'messages' => array("We can't check the updates at the moment"),
                'result' => '',
                'license' => 0,
                'latest' => 0,
            );
            $latest = false;

            if ($getinfo) {
                $response['messages'] = array();
                $licenseinfo = json_decode($getinfo);

                if (json_last_error() === 0) {

                    $license_type = isset($licenseinfo->license_type) ? $licenseinfo->license_type : 'venostore';

                    $response['version'] = isset($licenseinfo->version) ? $licenseinfo->version : VFM_VERSION;

                    if (version_compare(VFM_VERSION, $licenseinfo->version, '<')) {
                        $response['messages'][] = 'An updated version is available: <span class="badge rounded-pill bg-success">'.$licenseinfo->version.'</span>';
                    }
                    if (version_compare(VFM_VERSION, $licenseinfo->version, '>=')) {
                        $response['messages'][] = 'You have the latest version.';
                        $latest = true;
                        $response['latest'] = 1;
                    }

                    if (isset($licenseinfo->upgrade_notice)) {
                        $response['upgrade_notice'] = $licenseinfo->upgrade_notice;
                        $response['messages'][] = $licenseinfo->upgrade_notice;
                    }
                    if (isset($licenseinfo->logs) && $latest !== true) {
                        $response['logs'] = $licenseinfo->logs;
                    }
                    if ($setUp->getConfig("license_key")) {
                        if (isset($licenseinfo->license) && isset($licenseinfo->download_url)) {
                            $response['license'] = 1;
                        } else {
                            $response['messages'][] = 'Invalid license';
                        }
                    } else {
                        $response['messages'][] = 'Provide a license to activate automatic updates';
                    }
                } else {
                    $response['messages'][] = $getinfo;
                }
                if (isset($licenseinfo->error)) {
                    $response['messages'][] = $licenseinfo->error;
                }
            }
            return json_encode($response);
        }

        /**
         * Get package info
         *
         * @return downloaded package path
         */
        public function getInfo()
        {
            global $setUp;
            $query_args = array();
            $query_args['slug'] = $this->_slug;
            $query_args['site_url'] = urlencode($setUp->getConfig("script_url"));

            if ($setUp->getConfig("license_key")) {
                $query_args['license_key'] = $setUp->getConfig("license_key");
            }

            $getfile = $this->_update_url . '?action=get_metadata';
            foreach ($query_args as $key => $arg) {
                $getfile .= '&'.$key.'='. $arg;
            }
            $result = $this->getRemote($getfile);

            return $result;
        }

        /**
         * Get upgrade package
         *
         * @return downloaded package path
         */
        public function startUpdate()
        {
            $response = array(
                'error' => false,
                'result' => false,
            );

            $getinfo = $this->getInfo();

            if ($getinfo) {
                $licenseinfo = json_decode($getinfo);
                
                if (isset($licenseinfo->license) && isset($licenseinfo->download_url)) {
                    if (!$this->isRemoteFileZip($licenseinfo->download_url)) {
                        $response['error'] = array('Download not allowed');
                        return $response;
                    }

                    $getupgrade = $this->getRemote($licenseinfo->download_url);
                    return $this->downloadPackage($getupgrade);
                }
            }
            return $this->downloadPackage($result['response']);
        }

        /**
         * Get upgrade package
         *
         * @param string $package_data package data
         *
         * @return response
         */
        public function downloadPackage($package_data)
        {
            $response = array(
                'error' => false,
                'result' => false,
            );

            if ($package_data) {
                $upgradeDir = $this->_upgrade_dir;
                // Enable maintenance mode
                $fp = fopen(dirname($upgradeDir).'/.maintenance', 'w');

                if ($fp === false) {
                    $response['error'] = 'Failed enabling maintenance mode';
                    return $response;
                }

                fclose($fp);

                if (file_exists($upgradeDir) || mkdir($upgradeDir)) {
                    $package = $upgradeDir.'/'.$this->_package_zip;
                    if (false !== file_put_contents($package, $package_data)) {
                        $response['result'] = 'Package downloaded';
                    } else {
                        $response['error'] = 'Failed downloading the package';
                    }
                } else {
                    $response['error'] = 'Failed creating upgrade directory';
                }
            }
            return $response;
        }

        /**
         * Try to determine if a remote file is a zip.
         *
         * @param string $url url to check
         *
         * @return bool true if the remote file is a zip, false otherwise
         */
        public function isRemoteFileZip($url = false)
        {
            if (!$url) {
                return false;
            }
            $ch = curl_init($url);

            $headers = array(
                'Range: bytes=0-4',
                'Connection: close',
            );

            curl_setopt($ch, CURLOPT_HEADER, 0);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2450.0 Iron/46.0.2450.0');
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
            curl_setopt($ch, CURLOPT_VERBOSE, 0); // set to 1 to debug
            curl_setopt($ch, CURLOPT_STDERR, fopen('php://output', 'r'));

            $header = '';

            // write function that receives data from the response
            // aborts the transfer after reading 4 bytes of data
            curl_setopt(
                $ch,
                CURLOPT_WRITEFUNCTION,
                function ($curl, $data) use (&$header) {
                    $header .= $data;
                    if (strlen($header) < 4) {
                        return strlen($data);
                    }
                    return 0; // abort transfer.
                }
            );

            $result = curl_exec($ch);
            $info   = curl_getinfo($ch);

            // check for the zip magic header, return true if match, false otherwise
            return preg_match('/^PK(?:\x03\x04|\x05\x06|0x07\x08)/', $header);
        }

        /**
         * Unzip package
         *
         * @return expanded folder path
         */
        public function expandPackage()
        {
            $response = array(
                'error' => false,
                'result' => false,
            );

            $package = $this->_upgrade_dir.'/'.$this->_package_zip;

            if (file_exists($package)) {
                $expanded = dirname($package);
                $zip = new ZipArchive;
                $zipopen = $zip->open($package);
                if ($zipopen === true) {
                    $zip->extractTo($expanded.'/');
                    $zip->close();
                    // return $expanded;
                    $response['result'] = 'Package unzipped';
                }
            } else {
                $response['error'] = 'Failed unzipping archive';
            }
            return $response;
        }

        /**
         * Remove unused dirs and files
         *
         * @return expanded folder path
         */
        public function preparePackage()
        {
            $response = array(
                'error' => false,
                'result' => false,
            );
            $expanded = $this->_upgrade_dir;
            if (file_exists($expanded)) {
                $this->scanFolder($expanded.'/'.$this->_slug.'/uploads');
                $this->scanFolder($expanded.'/'.$this->_slug.'/vfm-admin/_content');
                $response['result'] = 'Ready to update files';
                $response['error'] = $this->_log;
            }
            return $response;
        }

        /**
         * Replace files
         *
         * @return expanded folder path
         */
        public function replaceFiles()
        {
            $response = array(
                'error' => false,
                'result' => false,
            );
            $expanded = $this->_upgrade_dir;
            if (file_exists($expanded)) {
                // Replace files.
                $this->scanFolder($expanded.'/'.$this->_slug, true);
                $response['result'] = 'Files replaced';
                $response['error'] = $this->_log;
            }
            return $response;
        }

        /**
         * End process
         *
         * @return expanded folder path
         */
        public function endProcess()
        {
            $response = array(
                'error' => false,
                'result' => false,
            );
            $expanded = $this->_upgrade_dir;
            if (file_exists($expanded)) {
                // Clean folder.
                $this->scanFolder($expanded);
            }
            // Disable Maintenance mode
            $maintenance = dirname($expanded).'/.maintenance';
            if (file_exists($maintenance)) {
                if (!unlink($maintenance)) {
                    $this->_log[] = 'Failed removing file: '.$maintenance.', please delete it via FTP';
                }
            }
            $response['result'] = 'End process';
            $response['error'] = $this->_log;
            return $response;
        }

        /**
         * Connect remote server
         *
         * @param string $url url to call
         *
         * @return response
         */
        public function getRemote($url)
        {
            global $setUp;
            $ch = curl_init($url);
            curl_setopt_array(
                $ch,
                array(
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_HEADER => false,
                    CURLOPT_SSL_VERIFYPEER => false,
                    CURLOPT_SSL_VERIFYHOST => false,
                    CURLOPT_FOLLOWLOCATION => false,
                )
            );

            $getcleanfile = curl_exec($ch);

            // Handle cURL error (timeout, DNS fail)
            if (curl_errno($ch)) {
                $response = curl_error($ch);
                if (is_resource($ch)) { curl_close($ch); } // Retrocompatibility PHP < 8.0
                return $response;
            }

            // Handle empty content
            if (false === $getcleanfile) {
                $response = $setUp->getString('error_downloading_remote_file');
                if (is_resource($ch)) { curl_close($ch); } // Retrocompatibility PHP < 8.0
                return $response;
            }

            // Clean closing for PHP < 8.0
            if (is_resource($ch)) {
                curl_close($ch);
            }
            
            // With PHP 8.0+, unsetting the variable CurlHandle gets destroyed by Garbage Collector
            unset($ch);

            return $getcleanfile;
        }

        /**
         * Scan directory
         *
         * @param string  $directory upgrader dir
         * @param boolean $replace   replace files
         *
         * @return process the update
         */
        public function scanFolder($directory = false, $replace = false)
        {
            $response = false;

            $marker = 'UPGRADE/vfm/';
            $script_dir = $this->_main_dir;

            if ($directory && file_exists($directory)) {

                foreach (glob($directory.'/{,.}*[!.]*', GLOB_MARK | GLOB_BRACE) as $file) {
                    if (is_dir($file)) {

                        $splitpath = explode($marker, $file);
                        $replacefile = $script_dir.'/'.$splitpath[1];

                        if (!file_exists($replacefile)) {
                            mkdir($replacefile);
                        }

                        $this->scanFolder(rtrim($file, '/\\'), $replace);
                    } else {
                        $this->checkFile($file, $replace);
                    }
                }
                rmdir($directory);
            }
        }

        /**
         * Replace file
         *
         * @param string  $file    upgrader dir
         * @param boolean $replace replace files
         *
         * @return process the update
         */
        public function checkFile($file, $replace = false)
        {
            if (!file_exists($file)) {
                $this->_log[] = 'File nout found: '.$file;
                return false;
            }
            
            $marker = 'UPGRADE/vfm/';
            $script_dir = $this->_main_dir;

            if ($replace === true) {
                $splitpath = explode($marker, $file);
                if (isset($splitpath[1])) {
                    $replacefile = $script_dir.'/'.$splitpath[1];
                    if (!file_exists($replacefile) || is_writable($replacefile)) {
                        // if (unlink($file) !== true) {
                        if (rename($file, $replacefile) !== true) {
                            $this->_log[] = 'Failed replacing file: '.$replacefile;
                        }
                    } else {
                        $this->_log[] = 'File not writeable: '.$replacefile;
                    }
                }
            } else {
                if (!unlink($file)) {
                    $this->_log[] = 'Failed removing file: '.$file;
                }
            }
        }

        /**
         * Check server settings
         *
         * @return response
         */
        public function serverCheck()
        {
            $required = array(
                'curl' => array(
                    'name' => 'cURL',
                    'enabled' => function_exists('curl_version'),
                    'ok' => 'cURL enabled',
                    'ko' => 'Please enable PHP cURL extension',
                ),
                'writable' => array(
                    'name' => 'cURL',
                    'enabled' => is_writable($this->_main_dir) && is_writable($this->_main_dir.'/vfm-admin'),
                    'ok' => 'Main directory writable',
                    'ko' => 'The script has no permissions to write inside the main directores: <br><code class="small">' .$this->_main_dir . '</code><br><code class="small">'.$this->_main_dir.'/vfm-admin</code>',
                ),
            );

            $response = array(
                'enabled' => true,
                'details' => array(),
            );

            foreach ($required as $key => $value) {
                $response['details'][$key]['enabled'] = $value['enabled'];
                if ($value['enabled'] !== true) {
                    $response['details'][$key]['text'] = $value['ko'];
                    $response['enabled'] = false;
                } else {
                    $response['details'][$key]['text'] = $value['ok'];
                }
            }
            return $response;
        }
    }
}

/**
 * Helper function to get/return the class object
 *
 * @return VFMUpdater object
 */
function VFM_updater()
{
    return VFMUpdater::getInstance();
}

// VFM_updater();
