<?php
/**
* Помощник установки ReadyScript. Закачивает и распаковывает необходимый 
* дистрибутив платформы для интернет-магазинов ReadyScript.
* 
* Зависимости: PHP 8.3+, права на запись в папку для PHP
* 
* Инструкция по установке:
* Загрузите данный файл на хостинг, в корневую папку вашего сайта по FTP
* и наберите в адресной строке браузера http://ВАШ-ДОМЕН.РУ/rs.php
* 
* @version 4.0
* @author ReadyScript lab. (https://readyscript.ru)
* @license https://readyscript.ru/licenseagreement/
*/
class AutoInstaller
{
    private
        /**
        * Адрес сервера ReadyScript
        */
        $rs_domain = 'https://readyscript.ru',
        $rs_backend_url,
        $rs_backend_relative_url = '/autoinstaller/',
        $tmp_filename = 'readyscript.tmp.zip',
        $lang = 'ru',
        $timeout = 3, //по умолчанию - 3 сек.
        $headers = array(),
        $file_chunk_size = 2000000, //по умолчанию - 2 Mb

        //Необязательные параметры, которые будут перенесены в файл локального окружения /_local_settings.php, создаваемый в корне сайта.
        //Здесь допустимо указывать только те свойства, которые объявлены в классе RS\AbstractSetup. Значения указанные здесь будут
        //перекрывать те, что заданы в RS\AbstractSetup
        $local_settings = array(
            'INSTALL_DB_HOST' => null, //null - означает - не изменять оригинальное значение
            'INSTALL_DB_PORT' => null, //null - означает - не изменять оригинальное значение
            'INSTALL_DB_NAME' => null,
            'INSTALL_DB_USERNAME' => null,
            'INSTALL_DB_PASSWORD' => null,
            'INSTALL_ADMIN_LOGIN' => null,
            'INSTALL_ADMIN_PASSWORD' => null,
            'INSTALL_SET_DEMO_DATA' => null,
            'ADMIN_SECTION' => null,
            'DISABLE_WIDGETS' => null
            //...
        );
    
    function __construct()
    {
        $this->rs_backend_url = $this->rs_domain.$this->rs_backend_relative_url;
        header('Content-type: text/html; charset=UTF-8');
    }
    
    /**
    * Запускает выполнение действия контроллера
    * 
    * @return mixed
    */
    public function run()
    {
        $action = 'action'.(isset($_GET['action']) ? $_GET['action'] : 'index');
        
        if (is_callable(array($this, $action))) {
            return $this->$action();
        }
    }
    
    /**
    * Отображает первую страницу установщика. Выбор редакции платформы.
    * 
    * @return void
    */
    public function actionIndex()
    {
        //Выполняем самотестирование
        if ($error = $this->selfTest()) {
            $this->displayError($error);
            return;
        }
        
        if ($request_result = $this->requester()) {
            $this->display($request_result->html, false);
        } else {
            $this->displayError('Не удалось соединиться с сервером ReadyScript');
        }
    }
    
    /**
    * Выполняет один шаг загрузки данных с сервера ReadyScript
    * 
    * @return void
    */
    public function actionAjaxDownloadPart()
    {
        $offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
        $script_type = isset($_GET['script_type']) ? $_GET['script_type'] : '';
        
        $result = $this->requester(array(
            'Act' => 'downloadChunk',
            'offset' => $offset,
            'package' => $script_type,
            'chunk_size' => $this->file_chunk_size
        ), false);
        
        if ($result !== false) {
            
            if (!$offset && file_exists($this->tmp_filename)) {
                unlink($this->tmp_filename);
            }
            
            if ($result !== '') {
                //Получена часть файла
                if (file_put_contents($this->tmp_filename, $result, FILE_APPEND)) {
                    //Продолжаем загрузку файла
                    $result_json = array(
                        'next' => array(
                            'action' => 'ajaxDownloadPart',
                            'script_type' => $script_type,
                            'offset' => $offset + $this->file_chunk_size
                        ),
                        'title' => 'Загрузка дистрибутива ReadyScript',
                        'percent' => round((filesize($this->tmp_filename) / $this->headers['Content-Length']) * 70) //Занимает 70% общего времени установки
                    );
                } else {
                    $result_json = array(
                        'error' => 'Не удалось записать данные на диск'
                    );
                }
            } else {
                //Достигнут конец файла, переходим к следующему действию
                $result_json = array(
                    'next' => array(
                        'action' => 'ajaxUnpack',
                    ),
                    'title' => 'Распаковка дистрибутива ReadyScript',
                    'percent' => 70
                );                
            }
        } else {
            $result_json = array(
                'error' => 'Потеряна связь с сервером ReadyScript'
            );
        }
        $this->display($result_json);
    }
    
    /**
    * Пошагово распаковывает загруженный дистрибутив ReadyScript в текущую папку
    * 
    * @return void
    */
    public function actionAjaxUnpack()
    {
        $offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
        
        $zip = new ZipArchive;
        if ($zip->open($this->tmp_filename) === true) {
            
            $time_start = microtime(true);
            for($i = $offset; $i < $zip->numFiles; $i++) {
                
                $file = $zip->getNameIndex($i);
                if (!$zip->extractTo(__DIR__, array($file))) {
                    $result_json = array(
                        'error' => 'Не удалось распаковать файл '.$file.'. Проверьте доступное место на диске (должно быть не менее 300 Мб).'
                    );
                    break;
                }
                
                if (((microtime(true)- $time_start) > $this->timeout) && ($i < $zip->numFiles - 1)) {
                    //Останаливаемся, чтобы не превышать лимит по времени выполнения скрипта                
                    $result_json = array(
                        'next' => array(
                            'action' => 'ajaxUnpack',
                            'offset' => $i+1
                        ),
                        'title' => 'Распаковка дистрибутива ReadyScript',
                        'percent' => 70 + round( (($i+1) / $zip->numFiles) * 28 ) //Занимает 28% общего времени установки
                    );
                    break;
                }
            }
            
            if (!isset($result_json)) {
                //Распаковка завершена успешно
                $result_json = array(
                    'next' => array(
                        'action' => 'ajaxChmod',
                    ),
                    'title' => 'Установка прав доступа к файлам и папкам',
                    'percent' => 97
                );                
            }
            $zip->close();
            
        } else {
            $result_json = array(
                'error' => 'Не удалось распаковать zip архив. Проверьте доступное место на диске (должно быть не менее 300 Мб).'
            );
        }
        
        $this->display($result_json);
    }
    
    /**
    * Устанавливает стандартные права доступа к файлам и папкам рекурсивно.
    * Всем папкам 0755, файлам 0644.
    * 
    * @return void
    */
    public function actionAjaxChmod()
    {
        $offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
        try {        
            $directoryIterator = new RecursiveDirectoryIterator(__DIR__, FilesystemIterator::SKIP_DOTS);
            $iterator = new RecursiveIteratorIterator($directoryIterator,  
                                                      RecursiveIteratorIterator::LEAVES_ONLY, 
                                                      RecursiveIteratorIterator::CATCH_GET_CHILD);
            
            $time_start = microtime(true);
            $i = 0;
            foreach($iterator as $name => $object) {
                if ($i < $offset) continue;
                
                if ($object->isDir()) {
                    @chmod($name, 0755);
                }
                if ($object->isFile()) {
                    @chmod($name, 0644);
                }
                
                if ((microtime(true)- $time_start) > $this->timeout) {
                    //Останаливаемся, чтобы не превышать лимит по времени выполнения скрипта                
                    $result_json = array(
                        'next' => array(
                            'action' => 'ajaxChmod',
                            'offset' => $i + 1
                        ),
                        'title' => 'Установка прав доступа к файлам и папкам',
                        'percent' => 99
                    );
                    break;
                }
            }
        } catch (Exception $e) {
            $result_json = array(
                'error' => $e->getMessage()
            );
        }
        
        if (!isset($result_json)) {
            $result_json = array(
                'next' => array(
                    'action' => 'ajaxDeleteSelf',
                ),
                'title' => 'Удаление временных файлов',
                'percent' => 100
            );
        }
        
        $this->display($result_json);
    }
    
    /**
    * Удаляет установочный файл и дает команду на редирект 
    * 
    * @return void
    */
    public function actionAjaxDeleteSelf()
    {
        if (!unlink(__FILE__)) {
            $result_json = array(
                'error' => 'Не удалось удалить установочный файл'
            );
        }
        
        elseif (!unlink($this->tmp_filename)) {
            $result_json = array(
                'error' => 'Не удалось удалить архив с дистрибутивом'
            );
        }

        elseif (!$this->createLocalSettings()) {
            $result_json = array(
                'error' => 'Не удалось создать файл с локальными настройками'
            );
        }
        
        else {
            $result_json = array(
                'finish' => true,
                'title' => 'Переход к установщику ReadyScript',
                'percent' => 100,
                'redirect' => '//'.$_SERVER['HTTP_HOST'].rtrim(dirname($_SERVER['REQUEST_URI']), '/\\')
            );
        }
        
        $this->display($result_json);
    }

    /**
    * Создает файл _local_settings.php в корне сайта
    *
    * @return bool
    */
    private function createLocalSettings()
    {
        $data = '';
        foreach($this->local_settings as $key => $value) {
            if ($value !== null) {
                $data .= '\Setup::$'.$key.'='.var_export($value, true).";\n";
            }
        }

        if ($data) {
            return @file_put_contents(__DIR__.'/_local_settings.php', "<?php\n".$data);
        }
        return true;
    }
    
    /**
    * Проводит тестирование возможности запуска помощника
    * 
    * @return bool(false) | string возвращает false, в случае отсутствия ошибок, иначе текст ошибки
    */
    private function selfTest()
    {
        //Проверяем версию PHP
        $php_version = phpversion();
        if (version_compare('8.3', $php_version) > 0) {
            return 'Версия PHP должна быть 8.3 или выше. У вас: '.$php_version;
        }
        
        //Проверяем не установлен ли уже ReadyScript в этой папке
        if (file_exists('setup.inc.php')) {
            return 'В текущей папке уже установлен продукт ReadyScript';
        }
        
        //Проверяем возможность записи на диск
        $tmp_file = 'write-test.tmp';
        $write_test = @file_put_contents($tmp_file, 'write-test') && unlink($tmp_file);
        if (!$write_test) {
            return 'Нет прав на запись в папку.';
        }
        
        //Проверяем возможность соединяться с удаленными хостами
        $allow_url_fopen = ini_get('allow_url_fopen');
        if (!$allow_url_fopen) {
            return 'Параметр <b>allow_url_fopen</b> должен иметь значение "<b>1</b>" в настройках PHP';
        }
        
        //Проверяем наличие распаковщика ZIP
        if (!class_exists('\ZipArchive')) {
            return 'Не установлен модуль ZIP для PHP';
        }
        
        return false;
    }
    
    /**
    * Отображает ошибку
    * 
    * @param string $error - текст ошибки
    * @return void
    */
    private function displayError($error)
    {
        $error = '
            <!DOCTYPE html>
            <html>
            <head>
            <meta http-equiv="Cache-Control" content="no-cache">
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <title>Установка ReadyScript</title>
            <link href="//fonts.googleapis.com/css?family=Open+Sans+Condensed:300" rel="stylesheet"> 
            <link rel="stylesheet" type="text/css" href="'.$this->rs_domain.'/modules/autoinstaller/view/css/style.css">
            </head>
            <body>
                <h1 class="welcome">Спасибо, что выбрали <span class="ready">Ready</span><span class="script">Script</span></h1>
                <p class="for-business">для вашего бизнеса</p>
                <div class="headline"></div>
                
                <section class="error">
                    <div class="error__message">
                        <div class="error__icon"></div>
                        '.$error.'
                    </div>
                    <a href="?" class="error__link">Попробуйте еще раз</a>
                </section>
            </body>
            </html>';
        $this->display($error, false);
    }
    
    /**
    * Отдает данные в браузер
    * 
    * @param mixed $data - готовый HTML код или подготовленный массив с JSON данными
    * @param bool $json - если true, то данные $data будут кодироваться в JSON
    */
    private function display($data, $json = true)
    {
        echo $json ? json_encode($data) : $data;
    }
    
    /**
    * Выполняет запрос на сервер ReadyScript
    * 
    * @param array $params - параметры запроса
    * @param bool $json_format - если true, то ответ от сервера ожидается в формате JSON
    * @return mixed
    */
    private function requester($params = array(), $json_format = true)
    {
        $context = stream_context_create(array(
            'http'=>array(
                'method' => "GET",
                'timeout' => 10
            )
        ));
        
        $params += array(
            'lang' => 'ru'
        );
        
        $uri_params = http_build_query($params);
        $result = @file_get_contents($this->rs_backend_url.'?'.$uri_params, null, $context);
        
        $this->headers = $this->parseHeaders($result !== false ? $http_response_header: array());
        
        if ($json_format) {
            return json_decode($result);
        }
        return $result;
    }
    
    /**
    * Парсит заголовки ответа сервера
    * 
    * @param string[] $headers
    * @return []
    */
    private function parseHeaders( $headers )
    {
        $head = array();
        foreach( $headers as $header ) {
            $t = explode( ':', $header, 2 );
            if( isset( $t[1] ) ) {
                $head[ trim($t[0]) ] = trim( $t[1] );
            }
        }
        return $head;
    }
}

$controller = new AutoInstaller();
$controller->run();