<?php

/**
 * Common utilities that might be useful in multiple projects.
 * Taken from DP dproofreaders/pinc/misc.inc.
 */

/**
 * Return whether the file is a valid zip file.
 *
 * The following conditions are used to check whether the file is considered a valid zip file:
 * - It must exist
 * - It must have the zip file extension
 * - It must be openable by ZipArchive
 *
 * If any of the conditions are not satisfied, it will return false, otherwise it will return true.
 * The extension check can be disabled by passing true as the second argument.
 */

class ZipException extends Exception
{
}

function is_valid_zip_file(string $file_path, bool $ignore_extension_check = false): bool
{
    try {
        validate_zip_file($file_path, $ignore_extension_check);
    } catch (ZipException $exception) {
        return false;
    }
    return true;
}

function validate_zip_file(string $file_path, bool $ignore_extension_check = false): void
{
    if (!file_exists($file_path)) {
        throw new ZipException("no file");
    }

    if (!$ignore_extension_check && !str_ends_with($file_path, '.zip')) {
        throw new ZipException("wrong extension");
    }

    $zip = new ZipArchive();

    $result = $zip->open($file_path);

    if ($result === true) {
        $zip->close();
    } else {
        $zip_results = [
            ZipArchive::ER_EXISTS => "File already exists.",
            ZipArchive::ER_INCONS => "Zip archive inconsistent.",
            ZipArchive::ER_INVAL => "Invalid argument.",
            ZipArchive::ER_MEMORY => "Malloc failure.",
            ZipArchive::ER_NOENT => "No such file.",
            ZipArchive::ER_NOZIP => "Not a zip archive.",
            ZipArchive::ER_OPEN => "Can't open file.",
            ZipArchive::ER_READ => "Read error.",
            ZipArchive::ER_SEEK => "Seek error.",
        ];
        $error = $zip_results[$result] ?? "Error code $result";
        throw new ZipException("ZipArchive::open() returned '$error'");
    }
}

/**
 * Return an array of files contained within the zip archive.
 *
 * It uses the is_valid_zip_file function to check whether the argument is a valid zip file, if it
 * is not, an InvalidArgumentException is thrown.
 */
function list_files_in_zip(string $file_path): array
{
    if (!is_valid_zip_file($file_path)) {
        throw new InvalidArgumentException(sprintf(
            _("Argument '%1\$s' is not a valid zip file."),
            $file_path
        ));
    }

    $zip = new ZipArchive();

    // No need to check the result of the function since it is checked in is_valid_zip_file
    $zip->open($file_path);

    $files = [];

    for ($i = 0; $i < $zip->numFiles; $i++) {
        $files[] = $zip->getNameIndex($i);
    }

    return $files;
}

/**
 * Extracts the zip directory's contents into the output directory.
 *
 * It uses the `is_valid_zip_file()` function to check whether the first argument is a valid zip file, if it
 * is not, an InvalidArgumentException is thrown.
 *
 * If the output directory does not exist or is not a valid directory, an InvalidArgumentException is thrown.
 *
 * Returns whether the extraction was successful.
 */
function extract_zip_to(string $path_to_zip, string $output_directory): bool
{
    if (!is_valid_zip_file($path_to_zip)) {
        throw new InvalidArgumentException(sprintf(
            _("Argument '%1\$s' is not a valid zip file."),
            $path_to_zip
        ));
    }

    if (!file_exists($output_directory) || !is_dir($output_directory)) {
        throw new InvalidArgumentException(sprintf(
            _("Argument '%1\$s' is not a valid output directory (it does not exist)."),
            $output_directory
        ));
    }

    $zip = new ZipArchive();

    // No need to check the result of the function since it is checked in is_valid_zip_file
    $zip->open($path_to_zip);

    return $zip->extractTo($output_directory);
}

/**
 * Remove a common base directory within a zip file if it exists
 *
 * Uses `is_valid_zip_file()` to check the zip file,
 * if it is not valid, throws an InvalidArgumentException.
 */
function remove_common_basedir_from_zip(string $path_to_zip): void
{
    if (!is_valid_zip_file($path_to_zip)) {
        throw new InvalidArgumentException(sprintf(
            _("Argument '%1\$s' is not a valid zip file."),
            $path_to_zip
        ));
    }

    $zip = new ZipArchive();
    $zip->open($path_to_zip);

    $filenames = [];
    $last_index = $zip->numFiles - 1;
    for ($i = 0; $i <= $last_index; $i++) {
        $filenames[] = $zip->getNameIndex($i);
    }

    sort($filenames);

    // If there is a common base directory there will be a common prefix
    // ending in /. If the zip was made in Linux or macos the directory itself
    // will be listed but not for windows.
    // If the files are sorted, then if the first contains a / there could be
    // a common directory prefix. Check if the last has this prefix. Then all
    // files in the archives are within the directory and we remove it from all
    // of them. If the directory itself is listed (must be first) delete it.
    $first_file = $filenames[0];
    $first_solidus = strpos($first_file, "/");
    if ($first_solidus !== false) {
        $basedir_len = $first_solidus + 1;
        $basedir = substr($first_file, 0, $basedir_len);
        if (str_starts_with($filenames[$last_index], $basedir)) {
            // we have a common subdirectory
            if (strlen($first_file) === $basedir_len) {
                // it is the dir name itself - delete it and remove basedir from others
                $zip->deleteName($first_file);
                $i = 1;
            } else {
                // remove basedir from all
                $i = 0;
            }
            while ($i <= $last_index) {
                $zip->renameName($filenames[$i], substr($filenames[$i], $basedir_len));
                $i++;
            }
        }
    }
    $zip->close();
}

/**
 * Creates a zip archive containing the specified files.
 *
 * The first argument represents the files to add to the zip archive and globing is allowed.
 * If there is a directory the files from it are added with its name prefixed (only one level)
 * The second argument represents the path to the zip archive which should be created.
 *
 * Returns whether the zip file was created successfully.
 *
 * Throws a InvalidArgumentException if:
 * - It cannot open a ZipArchive using the second argument as the path
 * - It cannot add one of the files specified by the first argument to the ZipArchive
 */
function create_zip_from(array $files_to_zip, string $path_to_zip): bool
{
    $zip = new ZipArchive();

    if ($zip->open($path_to_zip, ZipArchive::CREATE) !== true) {
        throw new InvalidArgumentException(sprintf(
            _("Could not open '%1\$s' as a ZipArchive."),
            $path_to_zip
        ));
    }

    foreach ($files_to_zip as $file) {
        if (is_dir($file)) {
            $status = $zip->addGlob("$file/*", 0, ['remove_all_path' => true, 'add_path' => basename($file) . "/"]);
        } else {
            $status = $zip->addGlob($file, 0, ['remove_all_path' => true]);
        }
        if (!$status) {
            throw new InvalidArgumentException(sprintf(_("Could not add '%1\$s' to '%2\$s' ZipArchive."), $file, $path_to_zip));
        }
    }

    return $zip->close();
}

/**
 * Return an error message appropriate to $upload_error_code.
 *
 * The wording is taken from "Handling File Uploads: Error Messages Explained"
 * in the PHP online documentation.
 */
function get_upload_err_msg(int $upload_error_code): string
{
    switch ($upload_error_code) {
        case UPLOAD_ERR_OK:
            return _('There is no error, the file uploaded with success.');
        case UPLOAD_ERR_INI_SIZE:
            return _('The uploaded file exceeds the upload_max_filesize directive in php.ini.');
        case UPLOAD_ERR_FORM_SIZE:
            return _('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.');
        case UPLOAD_ERR_PARTIAL:
            return _('The uploaded file was only partially uploaded.');
        case UPLOAD_ERR_NO_FILE:
            return _('No file was uploaded.');
        case UPLOAD_ERR_NO_TMP_DIR:
            return _('Missing a temporary folder.');
        case UPLOAD_ERR_CANT_WRITE:
            return _('Failed to write file to disk.');
        case UPLOAD_ERR_EXTENSION:
            return _('File upload stopped by extension.');
        default:
            return _('Unknown upload error code');
    }
}

/**
 * determines if a string is UTF-8.
 */
function is_utf8(string $str): bool
{
    if (function_exists('mb_check_encoding')) {
        return mb_check_encoding($str, 'UTF-8');
    }

    return preg_match('//u', $str) === 1;
}

/**
 * determines if a string has a UTF-8 byte order mark.
 */
function has_utf8_bom(string $str): bool
{
    return is_string($str) && strncmp($str, "\xEF\xBB\xBF", 3) === 0;
}
