<?php

/**
 * Bootstrap utility to exatract the core installer
 *
 * @package Duplicator\Installer
 *
 * Custom params
 *
 * [zipmode] to force extraction zip mode
 *      installer.php?zipmode=auto
 *      installer.php?zipmode=ziparchive
 *      installer.php?zipmode=shellexec
 *
 * [force-extract-installer] to force dup-installer folder overwrite
 *      installer.php?force-extract-installer=(1|on|yes)
 *
 * [dup_folder] to change dup-installer folder name
 *      installer.php?dup_folder=[custom_folder_name]
 *
 * [archive] to set custom archvie path location
 * can be fullpath with archive name or not
 *      installer.php?archive=[archive path]
 */

namespace {

    use Duplicator\Libs\DupArchive\DupArchiveExpandBasicEngine;

    $disabled_dirs = array(
        'backups-dup-lite',
        'wp-snapshots'
    );

    if (in_array(basename(dirname(__FILE__)), $disabled_dirs)) {
        die;
    }

    define('KB_IN_BYTES', 1024);
    define('MB_IN_BYTES', 1024 * KB_IN_BYTES);
    define('GB_IN_BYTES', 1024 * MB_IN_BYTES);
    define('DUPLICATOR_PHP_MAX_MEMORY', 4096 * MB_IN_BYTES);

    date_default_timezone_set('UTC'); // Some machines don’t have this set so just do it here.
    @ignore_user_abort(true);

    /**
     * Check if php.ini value is changeable
     *
     * @param string $setting php setting
     *
     * @return bool
     */
    function isIniValChangeable($setting)
    {
        static $ini_all;
        if (!isset($ini_all)) {
            $ini_all = false;
            // Sometimes `ini_get_all()` is disabled via the `disable_functions` option for "security purposes".
            if (function_exists('ini_get_all')) {
                $ini_all = ini_get_all();
            }
        }
        if (isset($ini_all[$setting]['access']) && ( INI_ALL === ( $ini_all[$setting]['access'] & 7 ) || INI_USER === ( $ini_all[$setting]['access'] & 7 ) )) {
            return true;
        }
        if (!is_array($ini_all)) {
            return true;
        }
        return false;
    }

    @set_time_limit(3600);
    if (isIniValChangeable('memory_limit')) {
        @ini_set('memory_limit', DUPLICATOR_PHP_MAX_MEMORY);
    }
    if (isIniValChangeable('max_input_time')) {
        @ini_set('max_input_time', '-1');
    }
    if (isIniValChangeable('pcre.backtrack_limit')) {
        @ini_set('pcre.backtrack_limit', PHP_INT_MAX);
    }
    if (isIniValChangeable('default_socket_timeout')) {
        @ini_set('default_socket_timeout', 3600);
    }

    LogHandler::init_error_handler();

    /**
     * Bootstrap
     */
    class DUPX_Bootstrap
    {
        //@@ Params get dynamically swapped when package is built
        const ARCHIVE_FILENAME   = '@@ARCHIVE@@';
        const ARCHIVE_SIZE       = '@@ARCHIVE_SIZE@@';
        const INSTALLER_DIR_NAME = 'dup-installer';
        const PACKAGE_HASH       = '@@PACKAGE_HASH@@';
        const SECONDARY_PACKAGE_HASH = '@@SECONDARY_PACKAGE_HASH@@';
        const VERSION            = '@@VERSION@@';

        const MINIMUM_PHP_VERSION = '5.3.8';

        const ZIP_MODE_AUTO    = 0;
        const ZIP_MODE_ARCHIVE = 1;
        const ZIP_MODE_SHELL   = 2;

        public $targetRoot            = null;
        public $origDupInstFolder     = null;
        public $targetDupInstFolder   = null;
        public $targetDupInst         = null;
        public $manualExtractFileName = null;
        public $isCustomDupFolder     = false;
        public $hasZipArchive         = false;
        public $hasShellExecUnzip     = false;
        public $mainInstallerURL;
        public $archiveExpectedSize   = 0;
        public $archiveActualSize     = 0;
        public $archiveRatio          = 0;

        /** @var self */
        private static $instance = null;

        /**
         * Instantiate the Bootstrap Object
         */
        private function __construct()
        {
            $this->setHTTPHeaders();
            $this->targetRoot        = self::setSafePath(dirname(__FILE__));
            // clean log file
            $this->log('', true);

            $archive_filepath = $this->getArchiveFilePath();
            $this->origDupInstFolder = self::INSTALLER_DIR_NAME;
            $this->targetDupInstFolder = filter_input(INPUT_GET, 'dup_folder', FILTER_SANITIZE_SPECIAL_CHARS, array(
                "options" => array(
                    "default" => self::INSTALLER_DIR_NAME,
                ),
                'flags'   => FILTER_FLAG_STRIP_HIGH));

            $this->isCustomDupFolder     = $this->origDupInstFolder !== $this->targetDupInstFolder;
            $this->targetDupInst         = $this->targetRoot . '/' . $this->targetDupInstFolder;
            $this->manualExtractFileName = 'dup-manual-extract__' . self::PACKAGE_HASH;

            if ($this->isCustomDupFolder) {
                $this->extractionTmpFolder = $this->getTempDir($this->targetRoot);
            } else {
                $this->extractionTmpFolder = $this->targetRoot;
            }

            DUPX_CSRF::init($this->targetDupInst, self::PACKAGE_HASH);

            //ARCHIVE_SIZE will be blank with a root filter so we can estimate
            //the default size of the package around 17.5MB (18088000)
            $archiveActualSize         = @file_exists($archive_filepath) ? @filesize($archive_filepath) : false;
            $archiveActualSize         = ($archiveActualSize !== false) ? $archiveActualSize : 0;
            $this->hasZipArchive       = class_exists('ZipArchive');
            $this->hasShellExecUnzip   = $this->getUnzipFilePath() != null ? true : false;
            $this->archiveExpectedSize = strlen(self::ARCHIVE_SIZE) ? self::ARCHIVE_SIZE : 0;
            $this->archiveActualSize   = $archiveActualSize;

            if ($this->archiveExpectedSize > 0) {
                $this->archiveRatio = (((1.0) * $this->archiveActualSize) / $this->archiveExpectedSize) * 100;
            } else {
                $this->archiveRatio = 100;
            }
        }

        /**
         *
         * @return self
         */
        public static function getInstance()
        {
            if (is_null(self::$instance)) {
                self::$instance = new self();
            }

            return self::$instance;
        }

        /**
         * Makes sure no caching mechanism is used during install
         *
         * @return void
         */
        private function setHTTPHeaders()
        {
            header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
            header("Cache-Control: post-check=0, pre-check=0", false);
            header("Pragma: no-cache");
        }

        /**
         * Return temp dir
         *
         * @param string $path path string
         *
         * @return boolean|string
         */
        private function getTempDir($path)
        {
            $tempfile = tempnam($path, 'dup-installer_tmp_');
            if (file_exists($tempfile)) {
                unlink($tempfile);
                mkdir($tempfile);
                if (is_dir($tempfile)) {
                    return $tempfile;
                }
            }
            return false;
        }

        /**
         * Check php version
         *
         * @return void
         */
        public static function phpVersionCheck()
        {
            if (version_compare(PHP_VERSION, self::MINIMUM_PHP_VERSION, '>=')) {
                return true;
            }

            $match = null;
            if (preg_match("#^\d+(\.\d+)*#", PHP_VERSION, $match)) {
                $phpVersion = $match[0];
            } else {
                $phpVersion = PHP_VERSION;
            }
            ?><!DOCTYPE html>
            <html>
                <head>
                    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
                    <meta name="robots" content="noindex,nofollow">
                    <title>Duplicator - issue</title>
                </head>
                <body>
                    <div>
                        <h1>DUPLICATOR ISSUE: PHP <?php echo self::MINIMUM_PHP_VERSION; ?> REQUIRED</h1>
                        <p>
                            This server is running PHP: <b><?php echo $phpVersion; ?></b>. <i>A minimum of <b>PHP 
                            <?php echo self::MINIMUM_PHP_VERSION; ?></b> is required</i>.<br><br>
                            <b>Contact your hosting provider or server administrator and let them know you would like to upgrade your PHP version.</b>
                        </p>
                    </div>
                </body>
            </html>
            <?php
            die();
        }

        /**
         * Run the bootstrap process which includes checking for requirements and running
         * the extraction process
         *
         * @return null | string Returns null if the run was successful otherwise an error message
         */
        public function run()
        {
            date_default_timezone_set('UTC'); // Some machines don't have this set so just do it here

            $this->log('==DUPLICATOR INSTALLER BOOTSTRAP v' . self::VERSION . '==');
            $this->log('----------------------------------------------------');
            $this->log('Installer bootstrap start');

            $archive_filepath = $this->getArchiveFilePath();
            $archive_filename = self::ARCHIVE_FILENAME;

            $error               = null;

            $is_installer_file_valid = true;
            if (preg_match('/_([a-z0-9]{7})[a-z0-9]+_[0-9]{6}([0-9]{8})_archive.(?:zip|daf)$/', $archive_filename, $matches)) {
                $expected_package_hash = $matches[1] . '-' . $matches[2];
                if (self::PACKAGE_HASH != $expected_package_hash) {
                    $is_installer_file_valid = false;
                    $this->log("[ERROR] Installer and archive mismatch detected.");
                }
            } else {
                $this->log("[ERROR] Invalid archive file name.");
                $is_installer_file_valid = false;
            }

            if (false  === $is_installer_file_valid) {
                $error = "Installer and archive mismatch detected.
                        Ensure uncorrupted installer and matching archive are present.";
                return $error;
            }

            $extract_installer   = true;
            $extract_success     = false;
            $archiveExpectedEasy = $this->readableByteSize($this->archiveExpectedSize);
            $archiveActualEasy   = $this->readableByteSize($this->archiveActualSize);

            //$archive_extension = strtolower(pathinfo($archive_filepath)['extension']);
            $archive_extension   = strtolower(pathinfo($archive_filepath, PATHINFO_EXTENSION));
            $installer_dir_found = (
                file_exists($this->targetDupInst) &&
                file_exists($this->targetDupInst . "/main.installer.php") &&
                file_exists($this->targetDupInst . "/dup-archive__" . self::PACKAGE_HASH . ".txt")
            );

            $manual_extract_found = (
                $installer_dir_found &&
                file_exists($this->targetDupInst . "/" . $this->manualExtractFileName)
            );

            $isZip = ($archive_extension == 'zip');

            //MANUAL EXTRACTION NOT FOUND
            if (!$manual_extract_found) {
                //MISSING ARCHIVE FILE
                if (!file_exists($archive_filepath)) {
                    $this->log("[ERROR] Archive file not found!");
                    $archive_candidates = ($isZip) ? $this->getFilesWithExtension('zip') : $this->getFilesWithExtension('daf');
                    $candidate_count    = count($archive_candidates);
                    $candidate_html     = "- No {$archive_extension} files found -";

                    if ($candidate_count >= 1) {
                        $candidate_html = "<ol>";
                        foreach ($archive_candidates as $archive_candidate) {
                            $candidate_html .= '<li class="diff-list"> ' . $this->compareStrings($archive_filename, $archive_candidate) . '</li>';
                        }
                        $candidate_html .= "</ol>";
                    }

                    $error = "<style>.diff-list font { font-weight: bold; }</style>"
                        . "<b>Archive not found!</b> The required archive file must be present in the <i>'Extraction Path'</i> below. "
                        . "When the archive file name was created it was named with a secure file name.  This file name must be "
                        . "the <i>exact same</i> name as when it was created character for character.  Each archive file has a unique installer associated "
                        . "with it and must be used together.  See the list below for more options:<br/>"
                        . "<ul>"
                        . "<li>If the archive is not finished downloading please wait for it to complete.</li>"
                        . "<li>Rename the file to it original hash name.  See WordPress-Admin ❯ Packages ❯  Details. </li>"
                        . "<li>When downloading, both files both should be from the same package line. </li>"
                        . "<li>Also see: <a href='https://duplicator.com/knowledge-base/how-to-fix-general-installer-ui-bootstrap-archive-issues' target='_blank'>"
                        . "How to fix various errors that show up before step-1 of the installer?</a></li>"
                        . "</ul>";

                    return $error;
                }

                $archive_size = self::ARCHIVE_SIZE;

                // Sometimes the self::ARCHIVE_SIZE is ''.
                if (!empty($archive_size) && !self::checkInputValidInt(self::ARCHIVE_SIZE)) {
                    $no_of_bits = PHP_INT_SIZE * 8;
                    $error      = 'Current is a ' . $no_of_bits . '-bit SO. This archive is too large for ' . $no_of_bits . '-bit PHP.' . '<br>';
                    $this->log('[ERROR] ' . $error);
                    $error      .= 'Possibibles solutions:<br>';
                    $error      .= '- Use the file filters to get your package lower to support this server or try the package on a Linux server.' . '<br>';
                    $error      .= '- Perform a <a target="_blank" href="https://duplicator.com/knowledge-base/how-to-handle-various-install-scenarios">' .
                        'Manual Extract Install</a>' . '<br>';

                    switch ($no_of_bits == 32) {
                        case 32:
                            $error .= '- Ask your host to upgrade the server to 64-bit PHP or install on another system has 64-bit PHP' . '<br>';
                            break;
                        case 64:
                            $error .= '- Ask your host to upgrade the server to 128-bit PHP or install on another system has 128-bit PHP' . '<br>';
                            break;
                    }

                    if (self::isWindows()) {
                        $error .= '- <a target="_blank" href="https://duplicator.com/knowledge-base/how-to-work-with-daf-files-and-the-duparchive-extraction-tool">' .
                            'Windows DupArchive extractor</a> to extract all files from the archive.' . '<br>';
                    }

                    return $error;
                }

                //SIZE CHECK ERROR
                if (($this->archiveRatio < 90) && ($this->archiveActualSize > 0) && ($this->archiveExpectedSize > 0)) {
                    $this->log(
                        "ERROR: The expected archive size should be around [{$archiveExpectedEasy}]. " .
                        "The actual size is currently [{$archiveActualEasy}]."
                    );
                    $this->log("ERROR: The archive file may not have fully been downloaded to the server");
                    $percent = round($this->archiveRatio);

                    $autochecked = isset($_POST['auto-fresh']) ? "checked='true'" : '';
                    $error       = "<b>Archive file size warning.</b><br/> The expected archive size is <b class='pass'>[{$archiveExpectedEasy}]</b>. "
                        . "Currently the archive size is <b class='fail'>[{$archiveActualEasy}]</b>. <br/>"
                        . "The archive file may have <b>not fully been uploaded to the server.</b>"
                        . "<ul>"
                        . "<li>Download the whole archive from the source website (open WordPress Admin &gt; Duplicator &gt; Packages) "
                        . "and validate that the file size is close to the expected size. </li>"
                        . "<li>Make sure to upload the whole archive file to the destination server.</li>"
                        . "<li>If the archive file is still uploading then please refresh this page to get an update on the currently uploaded file size.</li>"
                        . "</ul>";
                    return $error;
                }
            }

            if ($installer_dir_found) {
                // INSTALL DIRECTORY: Check if its setup correctly AND we are not in overwrite mode
                if (($extract_installer = filter_input(INPUT_GET, 'force-extract-installer', FILTER_VALIDATE_BOOLEAN))) {
                    $this->log("Manual extract found with force extract installer get parametr");
                } else {
                    $this->log("Manual extract found so not going to extract " . $this->targetDupInstFolder . " dir");
                }
            } else {
                $extract_installer = true;
            }

            // if ($extract_installer && file_exists($this->targetDupInst)) {
            if (file_exists($this->targetDupInst)) {
                $this->log("EXTRACT " . $this->targetDupInstFolder . " dir");
                $hash_pattern                 = '[a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9][a-z0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]';
                $file_patterns_with_hash_file = array(
                    // file pattern => hash file
                    'dup-archive__' . $hash_pattern . '.txt'        => 'dup-archive__' . self::PACKAGE_HASH . '.txt',
                    'dup-database__' . $hash_pattern . '.sql'       => 'dup-database__' . self::PACKAGE_HASH . '.sql',
                    'dup-installer-data__' . $hash_pattern . '.sql' => 'dup-installer-data__' . self::PACKAGE_HASH . '.sql',
                    'dup-installer-log__' . $hash_pattern . '.txt'  => 'dup-installer-log__' . self::PACKAGE_HASH . '.txt',
                    'dup-scan__' . $hash_pattern . '.json'          => 'dup-scan__' . self::PACKAGE_HASH . '.json',
                    'dup-scanned-dirs__' . $hash_pattern . '.txt'   => 'dup-scanned-dirs__' . self::PACKAGE_HASH . '.txt',
                    'dup-scanned-files__' . $hash_pattern . '.txt'  => 'dup-scanned-files__' . self::PACKAGE_HASH . '.txt',
                );
                foreach ($file_patterns_with_hash_file as $file_pattern => $hash_file) {
                    $globs = glob($this->targetDupInst . '/' . $file_pattern);
                    if (!empty($globs)) {
                        foreach ($globs as $glob) {
                            $file = basename($glob);
                            if ($file != $hash_file) {
                                if (unlink($glob)) {
                                    $this->log('Successfully deleted the file ' . $glob);
                                } else {
                                    $error .= '[ERROR] Error deleting the file ' . $glob . ' Please manually delete it and try again.';
                                    $this->log($error);
                                }
                            }
                        }
                    }
                }
            }

            //ATTEMPT EXTRACTION:
            //ZipArchive and Shell Exec
            if ($extract_installer) {
                $this->log("Ready to extract the installer");

                $this->log("Checking permission of destination folder");
                $destination = $this->targetRoot;
                if (!is_writable($destination)) {
                    $this->log("destination folder for extraction is not writable");
                    if (self::chmod($destination, 'u+rwx')) {
                        $this->log("Permission of destination folder changed to u+rwx");
                    } else {
                        $this->log("[ERROR] Permission of destination folder failed to change to u+rwx");
                    }
                }

                if (!is_writable($destination)) {
                    $this->log("WARNING: The {$destination} directory is not writable.");
                    $error = "NOTICE: The {$destination} directory is not writable on this server please talk to your host or server admin about making ";
                    $error .= "<a target='_blank' href='https://duplicator.com/knowledge-base/how-to-fix-file-permissions-issues'>" .
                        "writable {$destination} directory</a> on this server. <br/>";
                    return $error;
                }

                if ($isZip) {
                    $zip_mode = $this->getZipMode();

                    if (($zip_mode == self::ZIP_MODE_AUTO) || ($zip_mode == self::ZIP_MODE_ARCHIVE) && class_exists('ZipArchive')) {
                        if ($this->hasZipArchive) {
                            $this->log("ZipArchive exists so using that");
                            $extract_success = $this->extractInstallerZipArchive($archive_filepath, $this->origDupInstFolder, $this->extractionTmpFolder);

                            if ($extract_success) {
                                $this->log('Successfully extracted with ZipArchive');
                            } else {
                                if (0 == $this->installer_files_found) {
                                    $error = "[ERROR] This archive is not properly formatted and does not contain a " . $this->origDupInstFolder .
                                        " directory. Please make sure you are attempting to install " .
                                        "the original archive and not one that has been reconstructed.";
                                    $this->log($error);
                                    return $error;
                                } else {
                                    $error = '[ERROR] Error extracting with ZipArchive. ';
                                    $this->log($error);
                                }
                            }
                        } else {
                            $this->log("WARNING: ZipArchive is not enabled.");
                            $error = "NOTICE: ZipArchive is not enabled on this server please talk to your host or server admin about enabling ";
                            $error .= "<a target='_blank' href='https://duplicator.com/knowledge-base/how-to-work-with-the-different-zip-engines'>" .
                                "ZipArchive</a> on this server. <br/>";
                        }
                    }

                    if (!$extract_success) {
                        if (($zip_mode == self::ZIP_MODE_AUTO) || ($zip_mode == self::ZIP_MODE_SHELL)) {
                            $unzip_filepath = $this->getUnzipFilePath();
                            if ($unzip_filepath != null) {
                                $extract_success = $this->extractInstallerShellexec($archive_filepath, $this->origDupInstFolder, $this->extractionTmpFolder);
                                $this->log("Resetting perms of items in folder {$this->targetDupInstFolder}");
                                self::setPermsToDefaultR($this->targetDupInstFolder);
                                if ($extract_success) {
                                    $this->log('Successfully extracted with Shell Exec');
                                    $error = null;
                                } else {
                                    $error .= '[ERROR] Error extracting with Shell Exec. ' .
                                        'Please manually extract archive then choose Advanced > Manual Extract in installer.';
                                    $this->log($error);
                                }
                            } else {
                                $this->log('WARNING: Shell Exec Zip is not available');
                                $error .= "NOTICE: Shell Exec is not enabled on this server please talk to your host or server admin about enabling ";
                                $error .= "<a target='_blank' href='http://php.net/manual/en/function.shell-exec.php'>Shell Exec</a> " .
                                    "on this server or manually extract archive then choose Advanced > Manual Extract in installer.";
                            }
                        }
                    }

                    // If both ZipArchive and ShellZip are not available, Error message should be combined for both
                    if (!$extract_success && $zip_mode == self::ZIP_MODE_AUTO) {
                        $unzip_filepath = $this->getUnzipFilePath();
                        if (!class_exists('ZipArchive') && empty($unzip_filepath)) {
                            $this->log("WARNING: ZipArchive and Shell Exec are not enabled on this server.");
                            $error = "NOTICE: ZipArchive and Shell Exec are not enabled on this server please " .
                                "talk to your host or server admin about enabling ";
                            $error .= "<a target='_blank' href='https://duplicator.com/knowledge-base/how-to-work-with-the-different-zip-engines'>ZipArchive</a> " .
                                "or <a target='_blank' href='http://php.net/manual/en/function.shell-exec.php'>Shell Exec</a> " .
                                "on this server or manually extract archive then choose Advanced > Manual Extract in installer.";
                        }
                    }
                } else {
                    try {
                        DupArchiveExpandBasicEngine::setCallbacks(
                            array($this, 'log'),
                            array($this, 'chmod'),
                            array($this, 'mkdir')
                        );
                        $offset = DupArchiveExpandBasicEngine::getExtraOffset($archive_filepath);
                        $this->log('Expand directory from offset ' . $offset);
                        DupArchiveExpandBasicEngine::expandDirectory(
                            $archive_filepath,
                            $this->origDupInstFolder,
                            $this->extractionTmpFolder,
                            false,
                            $offset
                        );
                        //In case of DupArchive just remove the manual extract check file
                        @unlink($this->extractionTmpFolder . "/" . $this->origDupInstFolder . "/" . $this->manualExtractFileName);
                    } catch (Exception $ex) {
                        $this->log("[ERROR] Error expanding installer subdirectory:" . $ex->getMessage());
                        throw $ex;
                    }
                }

                if ($this->isCustomDupFolder) {
                    $this->log("Move dup-installer folder to custom folder:" .  $this->targetDupInst);
                    if (file_exists($this->targetDupInst)) {
                        $this->log('Custom folder already exists so delete it');
                        if (self::rrmdir($this->targetDupInst) == false) {
                            throw new Exception('Can\'t remove custom target folder');
                        }
                    }
                    if (rename($this->extractionTmpFolder . '/' . $this->origDupInstFolder, $this->targetDupInst) === false) {
                        throw new Exception('Can\'t rename the tmp dup-installer folder');
                    }
                }

                $htaccessToRemove = $this->targetDupInst.'/.htaccess';
                if (is_file($htaccessToRemove) && is_writable($htaccessToRemove)) {
                    $this->log("Remove Htaccess in dup-installer folder");
                    @unlink($htaccessToRemove);
                }

                $is_apache = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') !== false);
                $is_nginx  = (strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false);

                $sapi_type                   = php_sapi_name();
                $php_ini_data                = array(
                    'max_execution_time'     => 3600,
                    'max_input_time'         => -1,
                    'ignore_user_abort'      => 'On',
                    'post_max_size'          => '4096M',
                    'upload_max_filesize'    => '4096M',
                    'memory_limit'           => DUPLICATOR_PHP_MAX_MEMORY,
                    'default_socket_timeout' => 3600,
                    'pcre.backtrack_limit'   => 99999999999,
                );
                $sapi_type_first_three_chars = substr($sapi_type, 0, 3);
                if ('fpm' === $sapi_type_first_three_chars) {
                    $this->log("SAPI: FPM");
                    if ($is_apache) {
                        $this->log('Server: Apache');
                    } elseif ($is_nginx) {
                        $this->log('Server: Nginx');
                    }

                    if (($is_apache && function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules())) || $is_nginx) {
                        $htaccess_data = array();
                        foreach ($php_ini_data as $php_ini_key => $php_ini_val) {
                            if ($is_apache) {
                                $htaccess_data[] = 'SetEnv PHP_VALUE "' . $php_ini_key . ' = ' . $php_ini_val . '"';
                            } elseif ($is_nginx) {
                                if ('On' == $php_ini_val || 'Off' == $php_ini_val) {
                                    $htaccess_data[] = 'php_flag ' . $php_ini_key . ' ' . $php_ini_val;
                                } else {
                                    $htaccess_data[] = 'php_value ' . $php_ini_key . ' ' . $php_ini_val;
                                }
                            }
                        }

                        $htaccess_text      = implode("\n", $htaccess_data);
                        $htaccess_file_path = $this->targetDupInst . '/.htaccess';
                        $this->log("creating {$htaccess_file_path} with the content:");
                        $this->log($htaccess_text);
                        @file_put_contents($htaccess_file_path, $htaccess_text);
                    }
                } elseif ('cgi' === $sapi_type_first_three_chars || 'litespeed' === $sapi_type) {
                    if ('cgi' === $sapi_type_first_three_chars) {
                        $this->log("SAPI: CGI");
                    } else {
                        $this->log("SAPI: litespeed");
                    }
                    if (version_compare(phpversion(), 5.5) >= 0 && (!$is_apache || 'litespeed' === $sapi_type)) {
                        $ini_data = array();
                        foreach ($php_ini_data as $php_ini_key => $php_ini_val) {
                            $ini_data[] = $php_ini_key . ' = ' . $php_ini_val;
                        }
                        $ini_text      = implode("\n", $ini_data);
                        $ini_file_path = $this->targetDupInst . '/.user.ini';
                        $this->log("creating {$ini_file_path} with the content:");
                        $this->log($ini_text);
                        @file_put_contents($ini_file_path, $ini_text);
                    } else {
                        $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini");
                    }
                } elseif ("apache2handler" === $sapi_type) {
                    $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini");
                    $this->log("SAPI: apache2handler");
                }
                else {
                    $this->log("No need to create " . $this->targetDupInstFolder . "/.htaccess or " . $this->targetDupInstFolder . "/.user.ini");
                    $this->log("ERROR:  SAPI: Unrecognized");
                }
            } else {
                $this->log("NOTICE: Didn't need to extract the installer.");
            }

            if (empty($error)) {
                if ($this->isCustomDupFolder && file_exists($this->extractionTmpFolder)) {
                    rmdir($this->extractionTmpFolder);
                }

                $config_files              = glob($this->targetDupInst . '/dup-archive__*.txt');
                $config_file_absolute_path = array_pop($config_files);
                if (!file_exists($config_file_absolute_path)) {
                    $error = '<b>Archive config file not found in ' . $this->targetDupInstFolder . ' folder.</b> <br><br>';
                    return $error;
                }
            }

            $uri_start   = self::getCurrentUrl(false, false, 1);
            if ($error === null) {
                if (!file_exists($this->targetDupInst)) {
                    $error = 'Can\'t extract installer directory. ' .
                        'See <a target="_blank" href="https://duplicator.com/knowledge-base/how-to-fix-installer-archive-extraction-issues/">this FAQ item</a>' .
                        ' for details on how to resolve.</a>';
                }

                if ($error == null) {
                    $bootloader_name        = basename(__FILE__);
                    $this->mainInstallerURL = $uri_start . '/' . $this->targetDupInstFolder . '/main.installer.php';

                    $this->archive    = $archive_filepath;
                    $this->bootloader = $bootloader_name;

                    $this->fixInstallerPerms($this->mainInstallerURL);
                    // $this->mainInstallerURL = $this->mainInstallerURL . "?archive=$encoded_archive_path&bootloader=$bootloader_name&ctrl_action=ctrl-step1";
                    /*
                    if (isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) {
                        $this->mainInstallerURL .= '?'.$_SERVER['QUERY_STRING'];
                    }*/

                    $this->log("DONE: No detected errors so redirecting to the main installer. Main Installer URI = {$this->mainInstallerURL}");
                }
            }

            return $error;
        }

        /**
         * Get current url
         *
         * @param bool $queryString       If true the query string will also be returned.
         * @param bool $requestUri        if true check request uri
         * @param int  $getParentDirLevel if 0 get current script name or parent folder, if 1 parent folder if 2 parent of parent folder ...
         *
         * @return void
         */
        public static function getCurrentUrl($queryString = true, $requestUri = false, $getParentDirLevel = 0)
        {
            // *** HOST
            if (isset($_SERVER['HTTP_X_ORIGINAL_HOST'])) {
                $host = $_SERVER['HTTP_X_ORIGINAL_HOST'];
            } else {
                $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; //WAS SERVER_NAME and caused problems on some boxes
            }

            // *** PROTOCOL
            if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
                $_SERVER ['HTTPS'] = 'on';
            }
            if (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'https') {
                $_SERVER ['HTTPS'] = 'on';
            }
            if (isset($_SERVER['HTTP_CF_VISITOR'])) {
                $visitor = json_decode($_SERVER['HTTP_CF_VISITOR']);
                if ($visitor->scheme == 'https') {
                    $_SERVER ['HTTPS'] = 'on';
                }
            }
            $protocol = 'http' . ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') ? 's' : '');

            if ($requestUri) {
                $serverUrlSelf = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
            } else {
                // *** SCRIPT NAME
                $serverUrlSelf = $_SERVER['SCRIPT_NAME'];
                for ($i = 0; $i < $getParentDirLevel; $i++) {
                    $serverUrlSelf = preg_match('/^[\\\\\/]?$/', dirname($serverUrlSelf)) ? '' : dirname($serverUrlSelf);
                }
            }

            // *** QUERY STRING
            $query = ($queryString && isset($_SERVER['QUERY_STRING']) && strlen($_SERVER['QUERY_STRING']) > 0 ) ? '?' . $_SERVER['QUERY_STRING'] : '';

            return $protocol . '://' . $host . $serverUrlSelf . $query;
        }

        /**
         * Attempts to set the 'dup-installer' directory permissions
         *
         * @return null
         */
        private function fixInstallerPerms()
        {
            $file_perms = 'u+rw';
            $dir_perms  = 'u+rwx';

            $installer_dir_path = $this->targetDupInstFolder;

            $this->setPerms($installer_dir_path, $dir_perms, false);
            $this->setPerms($installer_dir_path, $file_perms, true);
        }

        /**
         * Set the permissions of a given directory and optionally all files
         *
         * @param string $directory The full path to the directory where perms will be set
         * @param string $perms     The given permission sets to use such as '0755' or 'u+rw'
         * @param string $do_files  Also set the permissions of all the files in the directory
         *
         * @return null
         */
        private function setPerms($directory, $perms, $do_files)
        {
            if (!$do_files) {
                // If setting a directory hiearchy be sure to include the base directory
                $this->setPermsOnItem($directory, $perms);
            }

            $item_names = array_diff(scandir($directory), array('.', '..'));

            foreach ($item_names as $item_name) {
                $path = "$directory/$item_name";
                if (($do_files && is_file($path)) || (!$do_files && !is_file($path))) {
                    $this->setPermsOnItem($path, $perms);
                }
            }
        }

        /**
         * Set the permissions of a single directory or file
         *
         * @param string $path  The full path to the directory or file where perms will be set
         * @param string $perms The given permission sets to use such as '0755' or 'u+rw'
         *
         * @return bool     Returns true if the permission was properly set
         */
        private function setPermsOnItem($path, $perms)
        {
            if (($result = self::chmod($path, $perms)) === false) {
                $this->log("ERROR: Couldn't set permissions of $path<br/>");
            } else {
                $this->log("Set permissions of $path<br/>");
            }
            return $result;
        }

        /**
         * Compare two strings and return html text which represts diff
         *
         * @param string $oldString old string
         * @param string $newString new string
         *
         * @return string Returns html text
         */
        private function compareStrings($oldString, $newString)
        {
            $ret = '';
            for ($i = 0; isset($oldString[$i]) || isset($newString[$i]); $i++) {
                if (!isset($oldString[$i])) {
                    $ret .= '<font color="red">' . $newString[$i] . '</font>';
                    continue;
                }
                for ($char = 0; isset($oldString[$i][$char]) || isset($newString[$i][$char]); $char++) {
                    if (!isset($oldString[$i][$char])) {
                        $ret .= '<font color="red">' . substr($newString[$i], $char) . '</font>';
                        break;
                    } elseif (!isset($newString[$i][$char])) {
                        break;
                    }

                    if (ord($oldString[$i][$char]) != ord($newString[$i][$char])) {
                        $ret .= '<font color="red">' . $newString[$i][$char] . '</font>';
                    } else {
                        $ret .= $newString[$i][$char];
                    }
                }
            }
            return $ret;
        }

        /**
         * Logs a string to the dup-installer-bootlog__[HASH].txt file
         *
         * @param string $s         The string to log to the log file
         * @param bool   $deleteOld if true delete old file
         *
         * @return boog|int This function returns the number of bytes that were written to the file, or FALSE on failure.
         */
        public function log($s, $deleteOld = false)
        {
            static $logfile = null;
            if (is_null($logfile)) {
                $logfile = $this->getBootLogFilePath();
            }
            if ($deleteOld && file_exists($logfile)) {
                @unlink($logfile);
            }
            $timestamp = date('M j H:i:s');
            return @file_put_contents($logfile, '[' . $timestamp . '] ' . self::postprocessLog($s) . "\n", FILE_APPEND);
        }

        /**
         * get boot log file name the dup-installer-bootlog__[HASH].txt file
         *
         * @return string
         */
        public function getBootLogFilePath()
        {
            return $this->targetRoot . '/dup-installer-bootlog__' . self::SECONDARY_PACKAGE_HASH . '.txt';
        }

        /**
         * Post process log and remove hash string
         *
         * @param string $str string
         *
         * @return string
         */
        protected static function postprocessLog($str)
        {
            return str_replace(array(
                self::getArchiveFileHash(),
                self::PACKAGE_HASH,
                self::SECONDARY_PACKAGE_HASH
                ), '[HASH]', $str);
        }

        /**
         * Return archive file hash
         *
         * @return string
         */
        public static function getArchiveFileHash()
        {
            static $fileHash = null;
            if (is_null($fileHash)) {
                $fileHash = preg_replace('/^.+_([a-z0-9]+)_[0-9]{14}_archive\.(?:daf|zip)$/', '$1', self::ARCHIVE_FILENAME);
            }
            return $fileHash;
        }

        /**
         * Extraxt installer
         *
         * @param string $archive_filepath  The path to the archive file.
         * @param string $origDupInstFolder relative folder in archive
         * @param string $destination       destination folder
         * @param bool   $checkSubFolder    check if is in subfolder
         *
         * @return bool Returns true if the data was properly extracted
         */
        private function extractInstallerZipArchive($archive_filepath, $origDupInstFolder, $destination, $checkSubFolder = false)
        {
            $success              = true;
            $zipArchive           = new ZipArchive();
            $subFolderArchiveList = array();

            if (($zipOpenRes = $zipArchive->open($archive_filepath)) === true) {
                $this->log("Successfully opened archive file.");
                $folder_prefix = $origDupInstFolder . '/';
                $this->log("Extracting all files from archive within " . $origDupInstFolder);

                $installer_files_found = 0;

                for ($i = 0; $i < $zipArchive->numFiles; $i++) {
                    $stat = $zipArchive->statIndex($i);
                    if ($checkSubFolder == false) {
                        $filenameCheck = $stat['name'];
                        $filename      = $stat['name'];
                        $tmpSubFolder  = null;
                    } else {
                        $safePath = rtrim(self::setSafePath($stat['name']), '/');
                        $tmpArray = explode('/', $safePath);

                        if (count($tmpArray) < 2) {
                            continue;
                        }

                        $tmpSubFolder  = $tmpArray[0];
                        array_shift($tmpArray);
                        $filenameCheck = implode('/', $tmpArray);
                        $filename      = $stat['name'];
                    }


                    if ($this->startsWith($filenameCheck, $folder_prefix)) {
                        $installer_files_found++;

                        if (!empty($tmpSubFolder) && !in_array($tmpSubFolder, $subFolderArchiveList)) {
                            $subFolderArchiveList[] = $tmpSubFolder;
                        }

                        if (basename($filename) === $this->manualExtractFileName) {
                            $this->log("Skipping manual extract file: {$filename}");
                            continue;
                        }

                        if ($zipArchive->extractTo($destination, $filename) === true) {
                            $this->log("Success: {$filename} >>> {$destination}");
                        } else {
                            $this->log("[ERROR] Error extracting {$filename} from archive archive file");
                            $success = false;
                            break;
                        }
                    }
                }

                if ($checkSubFolder && count($subFolderArchiveList) !== 1) {
                    $this->log("Error: Multiple dup subfolder archive");
                    $success = false;
                } else {
                    if ($checkSubFolder) {
                        $this->moveUpfromSubFolder($destination . '/' . $subFolderArchiveList[0], true);
                    }

                    $lib_directory     = $destination . '/' . $origDupInstFolder . '/lib';
                    $snaplib_directory = $lib_directory . '/snaplib';

                    // If snaplib files aren't present attempt to extract and copy those
                    if (!file_exists($snaplib_directory)) {
                        $folder_prefix = 'snaplib/';
                        $destination   = $lib_directory;

                        for ($i = 0; $i < $zipArchive->numFiles; $i++) {
                            $stat     = $zipArchive->statIndex($i);
                            $filename = $stat['name'];

                            if ($this->startsWith($filename, $folder_prefix)) {
                                $installer_files_found++;

                                if ($zipArchive->extractTo($destination, $filename) === true) {
                                    $this->log("Success: {$filename} >>> {$destination}");
                                } else {
                                    $this->log("[ERROR] Error extracting {$filename} from archive archive file");
                                    $success = false;
                                    break;
                                }
                            }
                        }
                    }
                }

                if ($zipArchive->close() === true) {
                    $this->log("Successfully closed archive file");
                } else {
                    $this->log("[ERROR] Problem closing archive file");
                    $success = false;
                }

                if ($success != false && $installer_files_found < 10) {
                    if ($checkSubFolder) {
                        $this->log("[ERROR] Couldn't find the installer directory in the archive!");
                        $success = false;
                    } else {
                        $this->log("[ERROR] Couldn't find the installer directory in archive root! Check subfolder");
                        $this->extractInstallerZipArchive($archive_filepath, $origDupInstFolder, $destination, true);
                    }
                }
            } else {
                $this->log("[ERROR] Couldn't open archive archive file with ZipArchive CODE[" . $zipOpenRes . "]");
                $success = false;
            }

            return $success;
        }

        /**
         * return true if current SO is windows
         *
         * @staticvar bool $isWindows
         * @return bool
         */
        public static function isWindows()
        {
            static $isWindows = null;
            if (is_null($isWindows)) {
                $isWindows = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
            }
            return $isWindows;
        }

        /**
         * return current SO path path len
         * @staticvar int $maxPath
         * @return int
         */
        public static function maxPathLen()
        {
            static $maxPath = null;
            if (is_null($maxPath)) {
                if (defined('PHP_MAXPATHLEN')) {
                    $maxPath = PHP_MAXPATHLEN;
                } else {
                    // for PHP < 5.3.0
                    $maxPath = self::isWindows() ? 260 : 4096;
                }
            }
            return $maxPath;
        }

        /**
         * @param string $directory Path for folder to set perms
         *
         * @return void
         */
        public static function setPermsToDefaultR($directory)
        {
            $dir      = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS);
            $iterator = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST);
            // Default permissions
            $defaultFilePermission = 0666 & ~umask();
            $defaultDirPermission  = 0777 & ~umask();

            foreach ($iterator as $item) {
                if ($item->isFile()) {
                    self::chmod($item->getPathname(), $defaultFilePermission);
                }

                if ($item->isDir()) {
                    self::chmod($item->getPathname(), $defaultDirPermission);
                }
            }
        }

        /**
         * this function make a chmod only if the are different from perms input and if chmod function is enabled
         *
         * this function handles the variable MODE in a way similar to the chmod of lunux
         * So the MODE variable can be
         * 1) an octal number (0755)
         * 2) a string that defines an octal number ("644")
         * 3) a string with the following format [ugoa]*([-+=]([rwx]*)+
         *
         * examples
         * u+rw         add read and write at the user
         * u+rw,uo-wx   add read and write ad the user and remove wx at groupd and other
         * a=rw         is equal at 666
         * u=rwx,go-rwx is equal at 700
         *
         * @param string     $file file path
         * @param int|string $mode mode
         *
         * @return boolean
         */
        public static function chmod($file, $mode)
        {
            if (!file_exists($file)) {
                return false;
            }

            $octalMode = 0;

            if (is_int($mode)) {
                $octalMode = $mode;
            } elseif (is_string($mode)) {
                $mode = trim($mode);
                if (preg_match('/([0-7]{1,3})/', $mode)) {
                    $octalMode = intval(('0' . $mode), 8);
                } elseif (preg_match_all('/(a|[ugo]{1,3})([-=+])([rwx]{1,3})/', $mode, $gMatch, PREG_SET_ORDER)) {
                    if (!function_exists('fileperms')) {
                        return false;
                    }

                    // start by file permission
                    $octalMode = (fileperms($file) & 0777);

                    foreach ($gMatch as $matches) {
                        // [ugo] or a = ugo
                        $group = $matches[1];
                        if ($group === 'a') {
                            $group = 'ugo';
                        }
                        // can be + - =
                        $action = $matches[2];
                        // [rwx]
                        $gPerms = $matches[3];

                        // reset octal group perms
                        $octalGroupMode = 0;

                        // Init sub perms
                        $subPerm = 0;
                        $subPerm += strpos($gPerms, 'x') !== false ? 1 : 0; // mask 001
                        $subPerm += strpos($gPerms, 'w') !== false ? 2 : 0; // mask 010
                        $subPerm += strpos($gPerms, 'r') !== false ? 4 : 0; // mask 100

                        $ugoLen = strlen($group);

                        if ($action === '=') {
                            // generate octal group permsissions and ugo mask invert
                            $ugoMaskInvert = 0777;
                            for ($i = 0; $i < $ugoLen; $i++) {
                                switch ($group[$i]) {
                                    case 'u':
                                        $octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000
                                        $ugoMaskInvert  = $ugoMaskInvert & 077;
                                        break;
                                    case 'g':
                                        $octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000
                                        $ugoMaskInvert  = $ugoMaskInvert & 0707;
                                        break;
                                    case 'o':
                                        $octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx
                                        $ugoMaskInvert  = $ugoMaskInvert & 0770;
                                        break;
                                }
                            }
                            // apply = action
                            $octalMode = $octalMode & ($ugoMaskInvert | $octalGroupMode);
                        } else {
                            // generate octal group permsissions
                            for ($i = 0; $i < $ugoLen; $i++) {
                                switch ($group[$i]) {
                                    case 'u':
                                        $octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000
                                        break;
                                    case 'g':
                                        $octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000
                                        break;
                                    case 'o':
                                        $octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx
                                        break;
                                }
                            }
                            // apply + or - action
                            switch ($action) {
                                case '+':
                                    $octalMode = $octalMode | $octalGroupMode;
                                    break;
                                case '-':
                                    $octalMode = $octalMode & ~$octalGroupMode;
                                    break;
                            }
                        }
                    }
                }
            }

            // if input permissions are equal at file permissions return true without performing chmod
            if (function_exists('fileperms') && $octalMode === (fileperms($file) & 0777)) {
                return true;
            }

            if (!function_exists('chmod')) {
                return false;
            }

            return @chmod($file, $octalMode);
        }

        /**
         * Check if input is  valid int
         *
         * @param mixed $input input string or number
         *
         * @return bool
         */
        public static function checkInputValidInt($input)
        {
            return (filter_var($input, FILTER_VALIDATE_INT) === 0 || filter_var($input, FILTER_VALIDATE_INT));
        }

        /**
         * this function creates a folder if it does not exist and performs a chmod.
         * it is different from the normal mkdir function to which an umask is applied to the input permissions.
         *
         * this function handles the variable MODE in a way similar to the chmod of lunux
         * So the MODE variable can be
         * 1) an octal number (0755)
         * 2) a string that defines an octal number ("644")
         * 3) a string with the following format [ugoa]*([-+=]([rwx]*)+
         *
         * @param string     $path      folder path
         * @param int|string $mode      mode permissions
         * @param bool       $recursive Allows the creation of nested directories specified in the pathname. Default to false.
         * @param resource   $context   not used for windows bug
         *
         * @return boolean bool TRUE on success or FALSE on failure.
         *
         * @todo check recursive true and multiple chmod
         */
        public static function mkdir($path, $mode = 0777, $recursive = false, $context = null)
        {
            if (strlen($path) > self::maxPathLen()) {
                throw new Exception('Skipping a file that exceeds allowed max path length [' . self::maxPathLen() . ']. File: ' . $filepath);
            }

            if (!file_exists($path)) {
                if (!function_exists('mkdir')) {
                    return false;
                }
                if (!@mkdir($path, 0777, $recursive)) {
                    return false;
                }
            }

            return self::chmod($path, $mode);
        }

        /**
         * move all folder content up to parent
         *
         * @param string  $subFolderName   full path
         * @param boolean $deleteSubFolder if true delete subFolder after moved all
         *
         * @return boolean
         */
        private function moveUpfromSubFolder($subFolderName, $deleteSubFolder = false)
        {
            if (!is_dir($subFolderName)) {
                return false;
            }

            $parentFolder = dirname($subFolderName);
            if (!is_writable($parentFolder)) {
                return false;
            }

            $success = true;
            if (($subList = glob(rtrim($subFolderName, '/') . '/*', GLOB_NOSORT)) === false) {
                $this->log("[ERROR] Problem glob folder " . $subFolderName);
                return false;
            } else {
                foreach ($subList as $cName) {
                    $destination = $parentFolder . '/' . basename($cName);
                    if (file_exists($destination)) {
                        $success = self::rrmdir($destination);
                    }

                    if ($success) {
                        $success = rename($cName, $destination);
                    } else {
                        break;
                    }
                }

                if ($success && $deleteSubFolder) {
                    $success = self::rrmdir($subFolderName, true);
                }
            }

            if (!$success) {
                $this->log("[ERROR] Problem om moveUpfromSubFolder subFolder:" . $subFolderName);
            }

            return $success;
        }

        /**
         * Extracts only the 'dup-installer' files using Shell-Exec Unzip
         *
         * @param string $archive_filepath  The path to the archive file.
         * @param string $origDupInstFolder dup-installer folder
         * @param string $destination       destination folder
         *
         * @return bool
         */
        private function extractInstallerShellexec($archive_filepath, $origDupInstFolder, $destination)
        {
            $success        = false;
            $this->log("Attempting to use Shell Exec");
            $unzip_filepath = $this->getUnzipFilePath();

            if ($unzip_filepath != null) {
                $unzip_command = "$unzip_filepath -q $archive_filepath " .
                    $origDupInstFolder . '/* -d ' . $destination . ' -x ' .
                    $origDupInstFolder . '/' . $this->manualExtractFileName . ' 2>&1';
                $this->log("Executing $unzip_command");
                $stderr        = shell_exec($unzip_command);

                $lib_directory     = $destination . '/' . $origDupInstFolder . '/lib';
                $snaplib_directory = $lib_directory . '/snaplib';

                // If snaplib files aren't present attempt to extract and copy those
                if (!file_exists($snaplib_directory)) {
                    $local_lib_directory = $destination . '/snaplib';
                    $unzip_command       = "$unzip_filepath -q $archive_filepath snaplib/* -d '.$destination.' 2>&1";

                    $this->log("Executing unzip command");
                    $stderr .= shell_exec($unzip_command);
                    self::mkdir($lib_directory, 'u+rwx');
                    rename($local_lib_directory, $snaplib_directory);
                }

                if ($stderr == '') {
                    $this->log("Shell exec unzip succeeded");
                    $success = true;
                } else {
                    $this->log("[ERROR] Shell exec unzip failed. Output={$stderr}");
                }
            }

            return $success;
        }

        /**
         * Attempts to get the archive file path
         *
         * @return string   The full path to the archive file
         */
        private function getArchiveFilePath()
        {
            if (($archive_filepath = filter_input(INPUT_GET, 'archive', FILTER_SANITIZE_SPECIAL_CHARS)) != false) {
                if (is_dir($archive_filepath) && file_exists($archive_filepath . '/' . self::ARCHIVE_FILENAME)) {
                    $archive_filepath = $archive_filepath . '/' . self::ARCHIVE_FILENAME;
                } else {
                    $archive_filepath = $archive_filepath;
                }
            } else {
                $archive_filepath = $this->targetRoot . '/' . self::ARCHIVE_FILENAME;
            }

            if (($realPath = realpath($archive_filepath)) !== false) {
                return $realPath;
            } else {
                return $archive_filepath;
            }
        }

        /**
         * Gets the enum type that should be used
         *
         * @return int Returns the current zip mode enum
         */
        private function getZipMode()
        {
            $zip_mode = self::ZIP_MODE_AUTO;

            if (isset($_GET['zipmode'])) {
                $zipmode_string = $_GET['zipmode'];
                $this->log("Unzip mode specified in querystring: $zipmode_string");

                switch ($zipmode_string) {
                    case 'autounzip':
                        $zip_mode = self::ZIP_MODE_AUTO;
                        break;
                    case 'ziparchive':
                        $zip_mode = self::ZIP_MODE_ARCHIVE;
                        break;
                    case 'shellexec':
                        $zip_mode = self::ZIP_MODE_SHELL;
                        break;
                }
            }

            return $zip_mode;
        }

        /**
         * Checks to see if a string starts with specific characters
         *
         * @param string $haystack haystack
         * @param string $needle   needle
         *
         * @return bool
         */
        private function startsWith($haystack, $needle)
        {
            return $needle === "" || strrpos($haystack, $needle, - strlen($haystack)) !== false;
        }

        /**
         * Checks to see if the server supports issuing commands to shell_exex
         *
         * @return bool     Returns true shell_exec can be ran on this server
         */
        public function hasShellExec()
        {
            $cmds = array('shell_exec', 'escapeshellarg', 'escapeshellcmd', 'extension_loaded');

            //Function disabled at server level
            if (array_intersect($cmds, array_map('trim', explode(',', @ini_get('disable_functions'))))) {
                return false;
            }

            //Suhosin: http://www.hardened-php.net/suhosin/
            //Will cause PHP to silently fail
            if (extension_loaded('suhosin')) {
                $suhosin_ini = @ini_get("suhosin.executor.func.blacklist");
                if (array_intersect($cmds, array_map('trim', explode(',', $suhosin_ini)))) {
                    return false;
                }
            }

            if (! function_exists('shell_exec')) {
                return false;
            }

            // Can we issue a simple echo command?
            if (!@shell_exec('echo duplicator')) {
                return false;
            }

            return true;
        }

        /**
         * Gets the possible system commands for unzip on Linux
         *
         * @return string       Returns unzip file path that can execute the unzip command
         */
        public function getUnzipFilePath()
        {
            $filepath = null;

            if ($this->hasShellExec()) {
                if (shell_exec('hash unzip 2>&1') == null) {
                    $filepath = 'unzip';
                } else {
                    $possible_paths = array(
                        '/usr/bin/unzip',
                        '/opt/local/bin/unzip',
                        '/bin/unzip',
                        '/usr/local/bin/unzip',
                        '/usr/sfw/bin/unzip',
                        '/usr/xdg4/bin/unzip',
                        '/opt/bin/unzip',
                        // RSR TODO put back in when we support shellexec on windows,
                    );

                    foreach ($possible_paths as $path) {
                        if (file_exists($path)) {
                            $filepath = $path;
                            break;
                        }
                    }
                }
            }

            return $filepath;
        }

        /**
         * Display human readable byte sizes such as 150MB
         *
         * @param int $size The size in bytes
         *
         * @return string A readable byte size format such as 100MB
         */
        public function readableByteSize($size)
        {
            try {
                $units = array('B', 'KB', 'MB', 'GB', 'TB');
                for ($i = 0; $size >= 1024 && $i < 4; $i++) {
                    $size  /= 1024;
                }
                return round($size, 2) . $units[$i];
            } catch (Exception $e) {
                return "n/a";
            }
        }

        /**
         * Returns an array of zip files found in the current executing directory
         *
         * @param string $extension extenstion file
         *
         * @return array of zip files
         */
        public function getFilesWithExtension($extension)
        {
            $files = array();
            foreach (glob("*.{$extension}") as $name) {
                if (file_exists($name)) {
                    $files[] = $name;
                }
            }
            if (count($files) > 0) {
                return $files;
            }
            //FALL BACK: Windows XP has bug with glob,
            //add secondary check for PHP lameness
            if (($dh = opendir($this->targetRoot))) {
                while (false !== ($name = readdir($dh))) {
                    $ext = substr($name, strrpos($name, '.') + 1);
                    if (in_array($ext, array($extension))) {
                        $files[] = $name;
                    }
                }
                closedir($dh);
            }

            return $files;
        }

        /**
         * Safely remove a directory and recursively files and directory upto multiple sublevels
         *
         * @param string $path The full path to the directory to remove
         *
         * @return bool Returns true if all content was removed
         */
        public static function rrmdir($path)
        {
            if (is_dir($path)) {
                if (($dh = opendir($path)) === false) {
                    return false;
                }
                while (($object = readdir($dh)) !== false) {
                    if ($object == "." || $object == "..") {
                        continue;
                    }
                    if (!self::rrmdir($path . "/" . $object)) {
                        closedir($dh);
                        return false;
                    }
                }
                closedir($dh);
                return @rmdir($path);
            } else {
                if (is_writable($path)) {
                    return @unlink($path);
                } else {
                    return false;
                }
            }
        }

        /**
         *  Makes path safe for any OS for PHP
         *
         *  Paths should ALWAYS READ be "/"
         *      uni:  /home/path/file.txt
         *      win:  D:/home/path/file.txt
         *
         *  @param string $path TThe path to make safe
         *
         *  @return string The original $path with a with all slashes facing '/'.
         */
        public static function setSafePath($path)
        {
            return str_replace("\\", "/", $path);
        }
    }

    class LogHandler
    {

        /** @var bool */
        private static $initialized = false;

        /**
         * This function only initializes the error handler the first time it is called
         *
         * @return void
         */
        public static function init_error_handler()
        {
            if (!self::$initialized) {
                @set_error_handler(array(__CLASS__, 'error'));
                @register_shutdown_function(array(__CLASS__, 'shutdown'));
                self::$initialized = true;
            }
        }

        /**
         * Error handler
         *
         * @param  integer $errno   Error level
         * @param  string  $errstr  Error message
         * @param  string  $errfile Error file
         * @param  integer $errline Error line
         * @return void
         */
        public static function error($errno, $errstr, $errfile, $errline)
        {
            switch ($errno) {
                case E_ERROR:
                    $log_message = self::getMessage($errno, $errstr, $errfile, $errline);
                    if (DUPX_Bootstrap::getInstance()->log($log_message) === false) {
                        $log_message = "Can\'t wrinte logfile\n\n" . $log_message;
                    }
                    die('<pre>' . htmlspecialchars($log_message) . '</pre>');
                    break;
                case E_NOTICE:
                case E_WARNING:
                default:
                    $log_message = self::getMessage($errno, $errstr, $errfile, $errline);
                    DUPX_Bootstrap::getInstance()->log($log_message);
                    break;
            }
        }

        /**
         * Get message from error
         *
         * @param int    $errno   errno
         * @param string $errstr  message
         * @param string $errfile file
         * @param int    $errline line
         *
         * @return string
         */
        private static function getMessage($errno, $errstr, $errfile, $errline)
        {
            $result = '[PHP ERR]';
            switch ($errno) {
                case E_ERROR:
                    $result .= '[FATAL]';
                    break;
                case E_WARNING:
                    $result .= '[WARN]';
                    break;
                case E_NOTICE:
                    $result .= '[NOTICE]';
                    break;
                default:
                    $result .= '[ISSUE]';
                    break;
            }
            $result .= ' MSG:';
            $result .= $errstr;
            $result .= ' [CODE:' . $errno . '|FILE:' . $errfile . '|LINE:' . $errline . ']';
            return $result;
        }

        /**
         * Shutdown handler
         *
         * @return void
         */
        public static function shutdown()
        {
            if (($error = error_get_last())) {
                LogHandler::error($error['type'], $error['message'], $error['file'], $error['line']);
            }
        }
    }

    class DUPX_CSRF
    {

        private static $packagHash = null;
        private static $mainFolder = null;

        /**
         * Session var name prefix
         * @var string
         */
        public static $prefix = '_DUPX_CSRF';

        /**
         * Stores all CSRF values: Key as CSRF name and Val as CRF value
         * @var array
         */
        private static $CSRFVars = null;

        /**
         * Init CSRF
         *
         * @param string $mainFolderm main folder
         * @param string $packageHash hash
         *
         * @return void
         */
        public static function init($mainFolderm, $packageHash)
        {
            self::$mainFolder = $mainFolderm;
            self::$packagHash = $packageHash;
            self::$CSRFVars   = null;
        }

        /**
         * Set new CSRF
         *
         * @param string $key CSRF Key
         * @param string $val CSRF Val
         *
         * @return Void
         */
        public static function setKeyVal($key, $val)
        {
            $CSRFVars       = self::getCSRFVars();
            $CSRFVars[$key] = $val;
            self::saveCSRFVars($CSRFVars);
            self::$CSRFVars = null;
        }

        /**
         * Get CSRF value by passing CSRF key
         *
         * @param string $key CSRF key
         *
         * @return string|boolean If CSRF value set for give n Key, It returns CRF value otherise returns false
         */
        public static function getVal($key)
        {
            $CSRFVars = self::getCSRFVars();
            if (isset($CSRFVars[$key])) {
                return $CSRFVars[$key];
            } else {
                return false;
            }
        }

        /**
         * Generate DUPX_CSRF value for form
         *
         * @param string $form Form name as session key
         *
         * @return  string token
         */
        public static function generate($form = null)
        {
            $keyName = self::getKeyName($form);

            $existingToken = self::getVal($keyName);
            if (false !== $existingToken) {
                $token = $existingToken;
            } else {
                $token = DUPX_CSRF::token() . DUPX_CSRF::fingerprint();
            }

            self::setKeyVal($keyName, $token);
            return $token;
        }

        /**
         * Check DUPX_CSRF value of form
         *
         * @param string $token Token
         * @param string $form  Form name as session key
         *
         * @return  boolean
         */
        public static function check($token, $form = null)
        {
            if (empty($form)) {
                return false;
            }

            $keyName  = self::getKeyName($form);
            $CSRFVars = self::getCSRFVars();
            if (isset($CSRFVars[$keyName]) && $CSRFVars[$keyName] == $token) { // token OK
                return true;
            }
            return false;
        }

        /** Generate token
         *
         * @return  string
         */
        protected static function token()
        {
            $microtime = (int) (microtime(true) * 10000);
            mt_srand($microtime);
            $charid = strtoupper(md5(uniqid(rand(), true)));
            return substr($charid, 0, 8) . substr($charid, 8, 4) . substr($charid, 12, 4) . substr($charid, 16, 4) . substr($charid, 20, 12);
        }

        /** Returns "digital fingerprint" of user
         *
         * @return  string  - MD5 hashed data
         */
        protected static function fingerprint()
        {
            return strtoupper(md5(implode('|', array($_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT']))));
        }

        /**
         * Generate CSRF Key name
         *
         * @param string $form the form name for which CSRF key need to generate
         * @return string CSRF key
         */
        private static function getKeyName($form)
        {
            return DUPX_CSRF::$prefix . '_' . $form;
        }

        /**
         * Get Package hash
         *
         * @return string Package hash
         */
        private static function getPackageHash()
        {
            if (is_null(self::$packagHash)) {
                throw new Exception('Not init CSFR CLASS');
            }
            return self::$packagHash;
        }

        /**
         * Get file path where CSRF tokens are stored in JSON encoded format
         *
         * @return string file path where CSRF token stored
         */
        private static function getFilePath()
        {
            if (is_null(self::$mainFolder)) {
                throw new Exception('Not init CSFR CLASS');
            }
            $dupInstallerfolderPath = self::$mainFolder;
            $packageHash            = self::getPackageHash();
            $fileName               = 'dup-installer-csrf__' . $packageHash . '.txt';
            $filePath               = $dupInstallerfolderPath . '/' . $fileName;
            return $filePath;
        }

        /**
         * Get all CSRF vars in array format
         *
         * @return array Key as CSRF name and value as CSRF value
         */
        private static function getCSRFVars()
        {
            if (is_null(self::$CSRFVars)) {
                $filePath = self::getFilePath();
                if (file_exists($filePath)) {
                    $contents = file_get_contents($filePath);
                    if (empty($contents)) {
                        self::$CSRFVars = array();
                    } else {
                        $CSRFobjs = json_decode($contents);
                        foreach ($CSRFobjs as $key => $value) {
                            self::$CSRFVars[$key] = $value;
                        }
                    }
                } else {
                    self::$CSRFVars = array();
                }
            }
            return self::$CSRFVars;
        }

        /**
         * Stores all CSRF vars
         *
         * @param array $CSRFVars holds all CSRF key val
         * @return void
         */
        private static function saveCSRFVars($CSRFVars)
        {
            $contents = json_encode($CSRFVars);
            $filePath = self::getFilePath();
            file_put_contents($filePath, $contents);
        }
    }
    /* * * CLASS DEFINITION END ** */
    $auto_refresh = isset($_POST['auto-fresh']) ? true : false;
    DUPX_Bootstrap::phpVersionCheck();

    try {
        $boot         = DUPX_Bootstrap::getInstance();
        $boot_error   = $boot->run();
    } catch (Exception $e) {
        $boot_error = $e->getMessage();
    }

    if ($boot_error == null) {
        $secure_csrf_token = DUPX_CSRF::generate('secure');
        $ctrl_csrf_token   = DUPX_CSRF::generate('ctrl-step1');
        DUPX_CSRF::setKeyVal('installerOrigCall', DUPX_Bootstrap::getCurrentUrl());
        DUPX_CSRF::setKeyVal('installerOrigPath', __FILE__);
        DUPX_CSRF::setKeyVal('archive', $boot->archive);
        DUPX_CSRF::setKeyVal('bootloader', $boot->bootloader);
        DUPX_CSRF::setKeyVal('booturl', '//' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
        DUPX_CSRF::setKeyVal('bootLogFile', $boot->getBootLogFilePath());
        DUPX_CSRF::setKeyVal('package_hash', DUPX_Bootstrap::PACKAGE_HASH);
        DUPX_CSRF::setKeyVal('secondaryHash', DUPX_Bootstrap::SECONDARY_PACKAGE_HASH);
    }
    ?>

    <html>
    <?php if ($boot_error == null) : ?>
        <head>
            <meta name="robots" content="noindex,nofollow">
            <title>Duplicator Installer</title>
            <link rel="icon" href="data:;base64,iVBORw0KGgo=">
        </head>
        <body>
            <div style="text-align: center; margin-top: 100px; font-size: 20px;">
                Initializing Installer. Please wait...
            </div>
            <?php
            $id   = uniqid();
            $html = "<form id='{$id}' method='post' action='{$boot->mainInstallerURL}' />\n";
            $data = array(
                'ctrl_action'     => 'ctrl-step1',
                'ctrl_csrf_token' => $ctrl_csrf_token,
                'step_action'     => 'init'
            );
            foreach ($data as $name => $value) {
                if ('csrf_token' != $name) {
                    $_SESSION[$name] = $value;
                }
                $html .= "<input type='hidden' name='{$name}' value='{$value}' />\n";
            }
            $html .= "</form>\n";
            $html .= "<script>window.onload = function() { document.getElementById('{$id}').submit(); }</script>";
            echo $html;
            ?>
        </body>
    <?php else : ?>
        <head>
            <link rel="icon" href="data:;base64,iVBORw0KGgo=">
            <style>
                body {font-family:Verdana,Arial,sans-serif; line-height:18px; font-size: 12px}
                h2 {font-size:20px; margin:5px 0 5px 0; border-bottom:1px solid #dfdfdf; padding:3px}
                div#content {
                    border:1px solid #CDCDCD; width:750px; min-height:550px; margin:auto; 
                    margin-top:18px; border-radius:5px; box-shadow:0 8px 6px -6px #333; font-size:13px;
                }
                div#content-inner {padding:10px 30px; min-height:550px}

                /* Header */
                table.header-wizard {
                    border-top-left-radius:5px; border-top-right-radius:5px; width:100%; 
                    box-shadow:0 5px 3px -3px #999; background-color:#F1F1F1; font-weight:bold;
                }
                table.header-wizard td.header {font-size:24px; padding:7px 0 7px 0; width:100%;}
                div.dupx-logfile-link {float:right; font-weight:normal; font-size:12px}
                .dupx-version {
                    white-space:nowrap; color:#999; font-size:11px; font-style:italic; text-align:right;  
                    padding:0 15px 5px 0; line-height:14px; font-weight:normal
                }
                .dupx-version a { color:#999; }

                div.errror-notice {text-align:center; font-style:italic; font-size:11px}
                div.errror-msg { color:maroon; padding: 10px 0 5px 0}
                .pass {color:green}
                .fail {color:red}
                span.file-info {font-size: 11px; font-style: italic}
                div.skip-not-found {padding:10px 0 5px 0;}
                div.skip-not-found label {cursor: pointer}
                table.settings {width:100%; font-size:12px}
                table.settings td {padding: 4px}
                table.settings td:first-child {font-weight: bold}
                .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{
                    color:#000!important;background-color:#f1f1f1!important
                }
                .w3-container:after,.w3-container:before,.w3-panel:after,
                .w3-panel:before,.w3-row:after,.w3-row:before,
                .w3-row-padding:after,.w3-row-padding:before,
                .w3-cell-row:before,.w3-cell-row:after,
                .w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after {
                    content:"";display:table;clear:both
                }
                .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
                .w3-container{padding:0.01em 16px}
                .w3-center{display:inline-block;width:auto; text-align: center !important}
            </style>
        </head>
        <body>
            <div id="content">

                <table cellspacing="0" class="header-wizard">
                    <tr>
                        <td class="header"> &nbsp; Duplicator - Bootloader</div</td>
                        <td class="dupx-version">
                            version: <?php echo htmlentities(DUPX_Bootstrap::VERSION); ?> <br/>
                            &raquo; 
                            <a target='_blank' href='dup-installer-bootlog__<?php echo DUPX_Bootstrap::SECONDARY_PACKAGE_HASH; ?>.txt'>
                                dup-installer-bootlog__[HASH].txt
                            </a>
                        </td>
                    </tr>
                </table>

                <form id="error-form" method="post">
                    <div id="content-inner">
                        <h2 style="color:maroon">Setup Notice</h2>
                        <div class="errror-notice">
                            An error has occurred. In order to load the full installer please resolve the issue below.
                        </div>
                        <div class="errror-msg">
                            <?php echo $boot_error ?>
                        </div>
                        <br/><br/>

                        <h2>Server Settings</h2>
                        <table class='settings'>
                            <tr>
                                <td>ZipArchive:</td>
                                <td><?php echo $boot->hasZipArchive ? '<i class="pass">Enabled</i>' : '<i class="fail">Disabled</i>'; ?> </td>
                            </tr>
                            <tr>
                                <td>Shell Unzip:</td>
                                <td><?php echo $boot->hasShellExecUnzip ? '<i class="pass">Enabled</i>' : '<i class="fail">Disabled</i>'; ?> </td>
                            </tr>
                            <tr>
                                <td>Extraction Path:</td>
                                <td><?php echo $boot->targetRoot; ?></td>
                            </tr>
                            <tr>
                                <td>Installer Path:</td>
                                <td><?php echo $boot->targetDupInstFolder; ?></td>
                            </tr>
                            <tr>
                                <td>Archive Size:</td>
                                <td>
                                    <b>Expected Size:</b> <?php echo $boot->readableByteSize($boot->archiveExpectedSize); ?>  &nbsp;
                                    <b>Actual Size:</b>   <?php echo $boot->readableByteSize($boot->archiveActualSize); ?>
                                </td>
                            </tr>
                            <tr>
                                <td>Boot Log</td>
                                <td>
                                    <a target='_blank' href='dup-installer-bootlog__<?php echo DUPX_Bootstrap::SECONDARY_PACKAGE_HASH; ?>.txt'>
                                        dup-installer-bootlog__[HASH].txt
                                    </a>
                                </td>
                            </tr>
                        </table>
                        <br/><br/>

                        <div style="font-size:11px">
                            Note: For archive.zip files either ZipArchive or Shell Exec will need to be enabled for the installer to run automatically
                            otherwise a manual extraction will need to be performed.  In order to run the installer manually follow the instructions to
                            <a href='https://duplicator.com/knowledge-base/how-to-handle-various-install-scenarios' target='_blank'>manually extract</a> before
                            running the installer.
                        </div>
                        <br/><br/>

                    </div>
                </form>

            </div>
        </body>

        <script>
            function AutoFresh() {
                document.getElementById('error-form').submit();
            }
        <?php if ($auto_refresh) : ?>
                var duration = 10000; //10 seconds
                var counter = 10;
                var countElement = document.getElementById('count-down');

                setTimeout(function () {
                    window.location.reload(1);
                }, duration);
                setInterval(function () {
                    counter--;
                    countElement.innerHTML = (counter > 0) ? counter.toString() : "0";
                }, 1000);

        <?php endif; ?>
        </script>


    <?php endif; ?>
    </html>
    <?php
}
@@DUPARCHIVE_MINI_EXPANDER@@