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

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

class SwitchboardDownloadPluginZip {

    public function __construct() {
        add_filter('plugin_action_links', [$this, 'add_download_link'], 10, 4);
        add_action('admin_action_download_plugin_zip', [$this, 'download_plugin']);
    }

    public function add_download_link($actions, $plugin_file, $plugin_data, $context) {
        if (!current_user_can('install_plugins')) {
            return $actions;
        }

        $url = wp_nonce_url(
            admin_url('admin.php?action=download_plugin_zip&plugin=' . urlencode($plugin_file)),
            'download_plugin_' . $plugin_file
        );

        $actions['download_zip'] = '<a href="' . esc_url($url) . '">' . esc_html__('Download ZIP', 'wpswitchboard') . '</a>';

        return $actions;
    }

    public function download_plugin() {
        if (!current_user_can('install_plugins')) {
            wp_die(esc_html__('You do not have permission to download plugins.', '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();

        $plugin_file = isset($_GET['plugin']) ? sanitize_text_field(wp_unslash($_GET['plugin'])) : '';

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

        check_admin_referer('download_plugin_' . $plugin_file);

        // Get plugin directory - handle both single-file and multi-file plugins
        // For multi-file plugins: "plugin-folder/plugin-file.php" -> "plugin-folder"
        // For single-file plugins: "plugin-file.php" -> "plugin-file.php" (download as single file)
        $plugin_path_parts = explode('/', $plugin_file);

        if (count($plugin_path_parts) > 1) {
            // Multi-file plugin in a directory
            $plugin_slug = $plugin_path_parts[0];
            $plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_slug;
            $is_single_file = false;
        } else {
            // Single-file plugin
            $plugin_slug = basename($plugin_file, '.php');
            $plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_file;
            $is_single_file = true;
        }

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

        // Verify the plugin exists
        if ($is_single_file) {
            if (!file_exists($plugin_dir) || !is_file($plugin_dir)) {
                wp_die(esc_html__('Plugin file not found.', 'wpswitchboard'));
            }
        } else {
            if (!is_dir($plugin_dir)) {
                wp_die(esc_html__('Plugin directory not found.', 'wpswitchboard'));
            }
        }

        // Create ZIP file
        $result = $this->create_zip($plugin_dir, $plugin_slug, $is_single_file);

        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($plugin_slug) . '.zip';
        $zip_handler = Switchboard_Zip_Handler::get_instance();
        $zip_handler->stream_file_to_browser($zip_file, $zip_file_name, true);
    }

    private function create_zip($source, $plugin_slug, $is_single_file = false) {
        $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 plugins
        @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 plugin slug for filename
        $safe_slug = sanitize_file_name($plugin_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 plugin size (e.g., "550MB")
                    __('Plugin size (%s) exceeds maximum allowed size (500MB). Please use FTP or another method to download this plugin.', '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 plugin directory
        $plugin_root = realpath(WP_PLUGIN_DIR);
        if ($plugin_root === false) {
            $plugin_root = str_replace('\\', '/', WP_PLUGIN_DIR);
        } else {
            $plugin_root = str_replace('\\', '/', $plugin_root);
        }

        if (strpos($source_normalized, $plugin_root) !== 0) {
            wp_delete_file($zip_file);
            return new WP_Error(
                'security_violation',
                __('Security check failed: Source is outside plugin 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, $is_single_file);
        } else {
            return $this->create_zip_pclzip($zip_file, $source, $safe_slug, $is_single_file);
        }
    }

    /**
     * Create ZIP using ZipArchive
     */
    private function create_zip_ziparchive($zip_file, $source, $safe_slug, $is_single_file) {
        $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_file($source)) {
            // Handle single-file plugin
            if (!$zip->addFile($source, $safe_slug . '.php')) {
                $zip->close();
                wp_delete_file($zip_file);
                return new WP_Error(
                    'zip_add_failed',
                    __('Failed to add plugin file to ZIP archive.', 'wpswitchboard')
                );
            }
            $file_count = 1;
        } elseif (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 plugin 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 plugin 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, $is_single_file) {
        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_file($source)) {
            $files_to_add[] = $source;
        } elseif (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 plugin 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 plugin directory may be empty or inaccessible.', 'wpswitchboard')
            );
        }

        if (is_file($source)) {
            $result = $zip->create($files_to_add, PCLZIP_OPT_REMOVE_PATH, dirname($source), PCLZIP_OPT_ADD_PATH, $safe_slug);
        } else {
            $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 SwitchboardDownloadPluginZip();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
