<?php

namespace HelpGent\WP_CLI;

use DirectoryIterator;
use Exception;
use PharData;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use HelpGent\WP_CLI;
use ZipArchive;
/**
 * Extract a provided archive file.
 */
class Extractor
{
    /**
     * Extract the archive file to a specific destination.
     *
     * @param string $dest
     */
    public static function extract($tarball_or_zip, $dest)
    {
        if (\preg_match('/\\.zip$/', $tarball_or_zip)) {
            return self::extract_zip($tarball_or_zip, $dest);
        }
        if (\preg_match('/\\.tar\\.gz$/', $tarball_or_zip)) {
            return self::extract_tarball($tarball_or_zip, $dest);
        }
        throw new Exception("Extraction only supported for '.zip' and '.tar.gz' file types.");
    }
    /**
     * Extract a ZIP file to a specific destination.
     *
     * @param string $zipfile
     * @param string $dest
     */
    private static function extract_zip($zipfile, $dest)
    {
        if (!\class_exists('ZipArchive')) {
            throw new Exception('Extracting a zip file requires ZipArchive.');
        }
        // Ensure the destination folder exists or can be created.
        if (!self::ensure_dir_exists($dest)) {
            throw new Exception("Could not create folder '{$dest}'.");
        }
        if (!\file_exists($zipfile) || !\is_readable($zipfile) || \filesize($zipfile) <= 0) {
            throw new Exception("Invalid zip file '{$zipfile}'.");
        }
        $zip = new ZipArchive();
        $res = $zip->open($zipfile);
        if (\true === $res) {
            $name = Utils\basename($zipfile);
            $tempdir = Utils\get_temp_dir() . \uniqid('wp-cli-extract-zipfile-', \true) . "-{$name}";
            $zip->extractTo($tempdir);
            $zip->close();
            self::copy_overwrite_files(self::get_first_subfolder($tempdir), $dest);
            self::rmdir($tempdir);
        } else {
            throw new Exception(\sprintf("ZipArchive failed to unzip '%s': %s.", $zipfile, self::zip_error_msg($res)));
        }
    }
    /**
     * Extract a tarball to a specific destination.
     *
     * @param string $tarball
     * @param string $dest
     */
    private static function extract_tarball($tarball, $dest)
    {
        // Ensure the destination folder exists or can be created.
        if (!self::ensure_dir_exists($dest)) {
            throw new Exception("Could not create folder '{$dest}'.");
        }
        if (\class_exists('PharData')) {
            try {
                $phar = new PharData($tarball);
                $name = Utils\basename($tarball);
                $tempdir = Utils\get_temp_dir() . \uniqid('wp-cli-extract-tarball-', \true) . "-{$name}";
                $phar->extractTo($tempdir);
                self::copy_overwrite_files(self::get_first_subfolder($tempdir), $dest);
                self::rmdir($tempdir);
                return;
            } catch (Exception $e) {
                WP_CLI::warning("PharData failed, falling back to 'tar xz' (" . $e->getMessage() . ')');
                // Fall through to trying `tar xz` below.
            }
        }
        // Ensure relative paths cannot be misinterpreted as hostnames.
        // Prepending `./` will force tar to interpret it as a filesystem path.
        if (self::path_is_relative($tarball)) {
            $tarball = "./{$tarball}";
        }
        if (!\file_exists($tarball) || !\is_readable($tarball) || \filesize($tarball) <= 0) {
            throw new Exception("Invalid zip file '{$tarball}'.");
        }
        // Note: directory must exist for tar --directory to work.
        $cmd = Utils\esc_cmd('tar xz --strip-components=1 --directory=%s -f %s', $dest, $tarball);
        $process_run = WP_CLI::launch(
            $cmd,
            \false,
            /*exit_on_error*/
            \true
        );
        if (0 !== $process_run->return_code) {
            throw new Exception(\sprintf('Failed to execute `%s`: %s.', $cmd, self::tar_error_msg($process_run)));
        }
    }
    /**
     * Copy files from source directory to destination directory. Source
     * directory must exist.
     *
     * @param string $source
     * @param string $dest
     */
    public static function copy_overwrite_files($source, $dest)
    {
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
        $error = 0;
        if (!\is_dir($dest)) {
            \mkdir($dest, 0777, \true);
        }
        foreach ($iterator as $item) {
            $dest_path = $dest . \DIRECTORY_SEPARATOR . $iterator->getSubPathName();
            if ($item->isDir()) {
                if (!\is_dir($dest_path)) {
                    \mkdir($dest_path);
                }
            } elseif (\file_exists($dest_path) && \is_writable($dest_path)) {
                \copy($item, $dest_path);
            } elseif (!\file_exists($dest_path)) {
                \copy($item, $dest_path);
            } else {
                $error = 1;
                WP_CLI::warning("Unable to copy '" . $iterator->getSubPathName() . "' to current directory.");
            }
        }
        if ($error) {
            throw new Exception('There was an error overwriting existing files.');
        }
    }
    /**
     * Delete all files and directories recursively from directory. Directory
     * must exist.
     *
     * @param string $dir
     */
    public static function rmdir($dir)
    {
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($files as $fileinfo) {
            $todo = $fileinfo->isDir() ? 'rmdir' : 'unlink';
            $path = $fileinfo->getRealPath();
            if (0 !== \strpos($path, $fileinfo->getRealPath())) {
                WP_CLI::warning("Temporary file or folder to be removed was found outside of temporary folder, aborting removal: '{$path}'");
            }
            $todo($path);
        }
        \rmdir($dir);
    }
    /**
     * Return formatted ZipArchive error message from error code.
     *
     * @param int $error_code
     * @return string|int The error message corresponding to the specified
     *                    code, if found; Other wise the same error code,
     *                    unmodified.
     */
    public static function zip_error_msg($error_code)
    {
        // From https://github.com/php/php-src/blob/php-5.3.0/ext/zip/php_zip.c#L2623-L2646.
        static $zip_err_msgs = [ZipArchive::ER_OK => 'No error', ZipArchive::ER_MULTIDISK => 'Multi-disk zip archives not supported', ZipArchive::ER_RENAME => 'Renaming temporary file failed', ZipArchive::ER_CLOSE => 'Closing zip archive failed', ZipArchive::ER_SEEK => 'Seek error', ZipArchive::ER_READ => 'Read error', ZipArchive::ER_WRITE => 'Write error', ZipArchive::ER_CRC => 'CRC error', ZipArchive::ER_ZIPCLOSED => 'Containing zip archive was closed', ZipArchive::ER_NOENT => 'No such file', ZipArchive::ER_EXISTS => 'File already exists', ZipArchive::ER_OPEN => 'Can\'t open file', ZipArchive::ER_TMPOPEN => 'Failure to create temporary file', ZipArchive::ER_ZLIB => 'Zlib error', ZipArchive::ER_MEMORY => 'Malloc failure', ZipArchive::ER_CHANGED => 'Entry has been changed', ZipArchive::ER_COMPNOTSUPP => 'Compression method not supported', ZipArchive::ER_EOF => 'Premature EOF', ZipArchive::ER_INVAL => 'Invalid argument', ZipArchive::ER_NOZIP => 'Not a zip archive', ZipArchive::ER_INTERNAL => 'Internal error', ZipArchive::ER_INCONS => 'Zip archive inconsistent', ZipArchive::ER_REMOVE => 'Can\'t remove file', ZipArchive::ER_DELETED => 'Entry has been deleted'];
        if (isset($zip_err_msgs[$error_code])) {
            return \sprintf('%s (%d)', $zip_err_msgs[$error_code], $error_code);
        }
        return $error_code;
    }
    /**
     * Return formatted error message from ProcessRun of tar command.
     *
     * @param Processrun $process_run
     * @return string|int The error message of the process, if available;
     *                    otherwise the return code.
     */
    public static function tar_error_msg($process_run)
    {
        $stderr = \trim($process_run->stderr);
        $nl_pos = \strpos($stderr, "\n");
        if (\false !== $nl_pos) {
            $stderr = \trim(\substr($stderr, 0, $nl_pos));
        }
        if ($stderr) {
            return \sprintf('%s (%d)', $stderr, $process_run->return_code);
        }
        return $process_run->return_code;
    }
    /**
     * Return the first subfolder within a given path.
     *
     * Falls back to the provided path if no subfolder was detected.
     *
     * @param string $path Path to find the first subfolder in.
     * @return string First subfolder, or same as $path if none found.
     */
    private static function get_first_subfolder($path)
    {
        $iterator = new DirectoryIterator($path);
        foreach ($iterator as $fileinfo) {
            if ($fileinfo->isDir() && !$fileinfo->isDot()) {
                return "{$path}/{$fileinfo->getFilename()}";
            }
        }
        return $path;
    }
    /**
     * Ensure directory exists.
     *
     * @param string $dir Directory to ensure the existence of.
     * @return bool Whether the existence could be asserted.
     */
    private static function ensure_dir_exists($dir)
    {
        if (!\is_dir($dir)) {
            if (!@\mkdir($dir, 0777, \true)) {
                $error = \error_get_last();
                WP_CLI::warning(\sprintf("Failed to create directory '%s': %s.", $dir, $error['message']));
                return \false;
            }
        }
        return \true;
    }
    /**
     * Check whether a path is relative-
     *
     * @param string $path Path to check.
     * @return bool Whether the path is relative.
     */
    private static function path_is_relative($path)
    {
        if ('' === $path) {
            return \true;
        }
        // Strip scheme.
        $scheme_position = \strpos($path, '://');
        if (\false !== $scheme_position) {
            $path = \substr($path, $scheme_position + 3);
        }
        // UNIX root "/" or "\" (Windows style).
        if ('/' === $path[0] || '\\' === $path[0]) {
            return \false;
        }
        // Windows root.
        if (\strlen($path) > 1 && \ctype_alpha($path[0]) && ':' === $path[1]) {
            // Special case: only drive letter, like "C:".
            if (2 === \strlen($path)) {
                return \false;
            }
            // Regular Windows path starting with drive letter, like "C:/ or "C:\".
            if ('/' === $path[2] || '\\' === $path[2]) {
                return \false;
            }
        }
        return \true;
    }
}
