<?php
/**
 * Name: Download Theme as ZIP
 * Description: Add download ZIP button on themes page to quickly export theme files for backup, migration, or local development without needing FTP or file access
 * Category: Utilities
 * Icon: -
 * UseCases: [
 *   {
 *     "title": "Quick Theme Backups",
 *     "description": "Easily download any theme as a ZIP file before updating or making changes, ensuring you have a backup copy in case something goes wrong."
 *   },
 *   {
 *     "title": "Share Custom Themes",
 *     "description": "Download custom-developed themes to share with other sites or clients without needing FTP access or command-line tools."
 *   },
 *   {
 *     "title": "Archive Theme Versions",
 *     "description": "Keep local archives of specific theme versions that work well with your setup, useful when updates cause compatibility issues."
 *   }
 * ]
 * Added: 2025-08-02
 * Tags: theme, download, zip, backup, export
 * WhereToFind: {
 *   "location": "Themes page",
 *   "url": "themes.php",
 *   "instructions": "Download ZIP button appears in theme details",
 *   "newTab": false
 * }
 */

// Prevent direct access
if (!defined('ABSPATH')) exit;

class SwitchboardDownloadThemeZip {

    public function __construct() {
        add_action('admin_action_download_theme_zip', [$this, 'download_theme']);
        add_action('admin_footer-themes.php', [$this, 'add_download_buttons']);
    }

    public function add_download_buttons() {
        if (!current_user_can('install_themes')) {
            return;
        }

        // Get all themes and create nonces
        $themes = wp_get_themes();
        $nonces = [];
        foreach ($themes as $slug => $theme) {
            $nonces[$slug] = wp_create_nonce('download_theme_' . $slug);
        }
        ?>
        <script>
        jQuery(document).ready(function($) {
            const nonces = <?php echo wp_json_encode($nonces); ?>;
            const baseUrl = <?php echo wp_json_encode(admin_url('admin.php?action=download_theme_zip')); ?>;

            // Add download button to each theme
            $('.theme').each(function() {
                const theme = $(this);
                const themeSlug = theme.attr('data-slug');

                // Skip "Add New Theme" button
                if (!themeSlug || theme.hasClass('add-new-theme') || !nonces[themeSlug]) {
                    return;
                }

                // Create download URL with nonce (escape for HTML context)
                const downloadUrl = baseUrl + '&theme=' + encodeURIComponent(themeSlug) + '&_wpnonce=' + encodeURIComponent(nonces[themeSlug]);

                // Find theme-actions div and add download button
                const themeActions = theme.find('.theme-actions');
                if (themeActions.length) {
                    // Create element safely to prevent XSS
                    const $link = $('<a></a>')
                        .attr('href', downloadUrl)
                        .attr('class', 'button')
                        .attr('title', 'Download ZIP')
                        .html('<span class="dashicons dashicons-download" style="margin-top: 3px;"></span>');
                    themeActions.append($link);
                }
            });
        });
        </script>
        <?php
    }

    public function download_theme() {
        if (!current_user_can('install_themes')) {
            wp_die(esc_html__('You do not have permission to download themes.', 'wpswitchboard'));
        }

        $zip_handler = Switchboard_Zip_Handler::get_instance();

        // Check system requirements using shared handler
        if (!$zip_handler->check_system_requirements()) {
            wp_die(esc_html__('System requirements not met. RecursiveIteratorIterator or zlib support is missing.', 'wpswitchboard'));
        }

        // Clean up old ZIP files using shared handler
        $zip_handler->cleanup_expired_files();

        $theme_slug = isset($_GET['theme']) ? sanitize_text_field(wp_unslash($_GET['theme'])) : '';

        if (empty($theme_slug)) {
            wp_die(esc_html__('No theme specified.', 'wpswitchboard'));
        }

        check_admin_referer('download_theme_' . $theme_slug);

        // Get theme directory
        $theme_dir = get_theme_root() . '/' . $theme_slug;

        // Validate theme slug to prevent path traversal - allow dots, alphanumeric, underscores, hyphens, spaces
        // Reject parent directory traversal (..), slashes, and null bytes
        if (empty($theme_slug) ||
            strpos($theme_slug, '..') !== false ||
            strpos($theme_slug, '/') !== false ||
            strpos($theme_slug, '\\') !== false ||
            strpos($theme_slug, "\0") !== false ||
            !preg_match('/^[a-z0-9._ -]+$/i', $theme_slug)) {
            wp_die(esc_html__('Invalid theme directory', 'wpswitchboard'));
        }

        if (!is_dir($theme_dir)) {
            wp_die(esc_html__('Theme directory not found.', 'wpswitchboard'));
        }

        // Create ZIP file
        $result = $this->create_zip($theme_dir, $theme_slug);

        if (is_wp_error($result)) {
            wp_die(
                sprintf(
                    '<strong>%s</strong><br><br>%s',
                    esc_html__('Failed to create ZIP file', 'wpswitchboard'),
                    esc_html($result->get_error_message())
                )
            );
        }

        $zip_file = $result;

        // Download the file using shared handler
        $zip_file_name = sanitize_file_name($theme_slug) . '.zip';
        $zip_handler->stream_file_to_browser($zip_file, $zip_file_name, true);
    }

    private function create_zip($source, $theme_slug) {
        $zip_handler = Switchboard_Zip_Handler::get_instance();

        // Check ZIP support using shared handler
        $zip_support = $zip_handler->check_zip_support();

        if (!$zip_support['available']) {
            return new WP_Error(
                'zip_not_available',
                __('ZIP extension is not available on this server. Please contact your hosting provider to enable the PHP ZIP extension or zlib support.', 'wpswitchboard')
            );
        }

        // Increase limits for large themes
        @ini_set('memory_limit', '512M'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- Intentional, ini_set may be disabled
        @set_time_limit(300); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- Intentional, set_time_limit may be disabled

        // Sanitize theme slug for filename
        $safe_slug = sanitize_file_name($theme_slug);

        // Calculate source size using shared handler
        $source_size = $zip_handler->get_directory_size($source);

        if ($source_size > Switchboard_Zip_Handler::MAX_SIZE) {
            return new WP_Error(
                'file_too_large',
                sprintf(
                    // translators: %s is the theme size (e.g., "550MB")
                    __('Theme size (%s) exceeds maximum allowed size (500MB). Please use FTP or another method to download this theme.', 'wpswitchboard'),
                    size_format($source_size)
                )
            );
        }

        // Check available disk space using shared handler
        if (!$zip_handler->has_disk_space($source_size)) {
            return new WP_Error(
                'insufficient_space',
                __('Insufficient disk space to create ZIP file. Please free up some space and try again.', 'wpswitchboard')
            );
        }

        // Find writable temp directory using shared handler
        $temp_dir = $zip_handler->find_writable_temp_dir($source_size * 2);

        if (!$temp_dir) {
            return new WP_Error(
                'no_writable_temp_dir',
                __('Cannot create temporary ZIP file. Please contact your hosting provider to check directory permissions.', 'wpswitchboard')
            );
        }

        $zip_file = trailingslashit($temp_dir) . $safe_slug . '-' . wp_generate_password(8, false) . '.zip';

        // Normalize source path and verify security
        $source_normalized = realpath($source);
        if ($source_normalized === false) {
            $source_normalized = str_replace('\\', '/', $source);
        } else {
            $source_normalized = str_replace('\\', '/', $source_normalized);
        }

        // Security: Verify source is within theme directory
        $theme_root = realpath(get_theme_root());
        if ($theme_root === false) {
            $theme_root = str_replace('\\', '/', get_theme_root());
        } else {
            $theme_root = str_replace('\\', '/', $theme_root);
        }

        if (strpos($source_normalized, $theme_root) !== 0) {
            wp_delete_file($zip_file);
            return new WP_Error(
                'security_violation',
                __('Security check failed: Source is outside theme directory.', 'wpswitchboard')
            );
        }

        $source = $source_normalized;

        // Use shared handler methods based on available ZIP method
        if ($zip_support['method'] === 'ZipArchive') {
            return $this->create_zip_ziparchive($zip_file, $source, $safe_slug);
        } else {
            return $this->create_zip_pclzip($zip_file, $source, $safe_slug);
        }
    }

    /**
     * Create ZIP using ZipArchive
     */
    private function create_zip_ziparchive($zip_file, $source, $safe_slug) {
        $zip = new ZipArchive();

        if ($zip->open($zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            return new WP_Error(
                'zip_create_failed',
                sprintf(
                    // translators: %s is the file path where ZIP creation failed
                    __('Failed to create ZIP archive at %s. The disk may be full or permissions may be incorrect.', 'wpswitchboard'),
                    $zip_file
                )
            );
        }

        $file_count = 0;

        if (is_dir($source)) {
            try {
                $files = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
                    RecursiveIteratorIterator::LEAVES_ONLY,
                    RecursiveIteratorIterator::CATCH_GET_CHILD
                );

                foreach ($files as $file) {
                    if (!$file->isReadable()) {
                        continue;
                    }

                    if (!$file->isDir()) {
                        $file_path = $file->getRealPath();
                        $file_path = str_replace('\\', '/', $file_path);
                        $source_normalized = str_replace('\\', '/', $source);

                        if ($file_path === false || strpos($file_path, $source_normalized) !== 0) {
                            continue;
                        }

                        $relative_path = $safe_slug . '/' . substr($file_path, strlen($source_normalized) + 1);

                        if (@$zip->addFile($file_path, $relative_path)) {
                            $file_count++;
                        }
                    }
                }
            } catch (Exception $e) {
                $zip->close();
                wp_delete_file($zip_file);
                return new WP_Error(
                    'directory_iteration_failed',
                    sprintf(
                        // translators: %s is the error message from the directory iteration failure
                        __('Failed to read theme directory: %s', 'wpswitchboard'),
                        $e->getMessage()
                    )
                );
            }
        }

        if ($file_count === 0) {
            $zip->close();
            wp_delete_file($zip_file);
            return new WP_Error(
                'no_files_added',
                __('No files were added to the ZIP archive. The theme directory may be empty or inaccessible.', 'wpswitchboard')
            );
        }

        $zip->close();

        if (!file_exists($zip_file) || filesize($zip_file) === 0) {
            wp_delete_file($zip_file);
            return new WP_Error(
                'zip_creation_failed',
                __('ZIP file was not created successfully or is empty.', 'wpswitchboard')
            );
        }

        return $zip_file;
    }

    /**
     * Create ZIP using PclZip (WordPress built-in fallback)
     */
    private function create_zip_pclzip($zip_file, $source, $safe_slug) {
        if (!class_exists('PclZip')) {
            require_once(ABSPATH . 'wp-admin/includes/class-pclzip.php');
        }

        if (!class_exists('PclZip')) {
            return new WP_Error(
                'pclzip_not_available',
                __('PclZip class is not available. Please contact your hosting provider.', 'wpswitchboard')
            );
        }

        $zip = new PclZip($zip_file);
        $files_to_add = [];

        if (is_dir($source)) {
            try {
                $files = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
                    RecursiveIteratorIterator::LEAVES_ONLY,
                    RecursiveIteratorIterator::CATCH_GET_CHILD
                );

                foreach ($files as $file) {
                    if (!$file->isDir() && $file->isReadable()) {
                        $file_path = $file->getRealPath();

                        if ($file_path !== false) {
                            $file_path = str_replace('\\', '/', $file_path);
                            $source_normalized = str_replace('\\', '/', $source);

                            if (strpos($file_path, $source_normalized) === 0) {
                                $files_to_add[] = $file_path;
                            }
                        }
                    }
                }
            } catch (Exception $e) {
                wp_delete_file($zip_file);
                return new WP_Error(
                    'directory_iteration_failed',
                    sprintf(
                        // translators: %s is the error message from the directory iteration failure
                        __('Failed to read theme directory: %s', 'wpswitchboard'),
                        $e->getMessage()
                    )
                );
            }
        }

        if (empty($files_to_add)) {
            wp_delete_file($zip_file);
            return new WP_Error(
                'no_files_added',
                __('No files were added to the ZIP archive. The theme directory may be empty or inaccessible.', 'wpswitchboard')
            );
        }

        $result = $zip->create($files_to_add, PCLZIP_OPT_REMOVE_PATH, $source, PCLZIP_OPT_ADD_PATH, $safe_slug);

        if ($result == 0) {
            $error = $zip->errorInfo(true);
            wp_delete_file($zip_file);
            return new WP_Error(
                'zip_create_failed',
                sprintf(
                    // translators: %s is the error message from the ZIP creation failure
                    __('Failed to create ZIP archive: %s', 'wpswitchboard'),
                    $error
                )
            );
        }

        if (!file_exists($zip_file) || filesize($zip_file) === 0) {
            wp_delete_file($zip_file);
            return new WP_Error(
                'zip_creation_failed',
                __('ZIP file was not created successfully or is empty.', 'wpswitchboard')
            );
        }

        return $zip_file;
    }

    /**
     * Register data locations for cleanup system
     */
    public function register_data_locations() {
        return [
            'options' => [],
            'option_patterns' => [],
            'tables' => [],
            'post_meta' => [],
            'user_meta' => [],
        ];
    }
}

// Initialize and register with data system
$switchboard_instance = new SwitchboardDownloadThemeZip();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
