get_charset_collate(); $versions_table = $wpdb->prefix . self::$versions_table; $downloads_table = $wpdb->prefix . self::$downloads_table; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // Software versions table $sql_versions = "CREATE TABLE $versions_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, product_id bigint(20) UNSIGNED NOT NULL, version varchar(50) NOT NULL, changelog longtext, release_date datetime NOT NULL, is_current tinyint(1) DEFAULT 0, download_count int(11) DEFAULT 0, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_product (product_id), KEY idx_current (product_id, is_current), UNIQUE KEY unique_version (product_id, version) ) $charset_collate;"; dbDelta($sql_versions); // Download tokens table (for secure downloads) $sql_downloads = "CREATE TABLE $downloads_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, token varchar(64) NOT NULL, license_id bigint(20) UNSIGNED NOT NULL, product_id bigint(20) UNSIGNED NOT NULL, version_id bigint(20) UNSIGNED, ip_address varchar(45), expires_at datetime NOT NULL, used_at datetime DEFAULT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY token (token), KEY license_id (license_id), KEY expires_at (expires_at) ) $charset_collate;"; dbDelta($sql_downloads); } /** * Get product software configuration */ public static function get_product_config($product_id) { $enabled = get_post_meta($product_id, '_woonoow_software_enabled', true) === 'yes'; if (!$enabled) { return null; } return [ 'enabled' => true, 'slug' => get_post_meta($product_id, '_woonoow_software_slug', true), 'current_version' => get_post_meta($product_id, '_woonoow_software_current_version', true), 'wp_enabled' => get_post_meta($product_id, '_woonoow_software_wp_enabled', true) === 'yes', 'requires_wp' => get_post_meta($product_id, '_woonoow_software_requires_wp', true), 'tested_wp' => get_post_meta($product_id, '_woonoow_software_tested_wp', true), 'requires_php' => get_post_meta($product_id, '_woonoow_software_requires_php', true), 'icon' => get_post_meta($product_id, '_woonoow_software_icon', true), 'banner' => get_post_meta($product_id, '_woonoow_software_banner', true), ]; } /** * Get product by software slug */ public static function get_product_by_slug($slug) { global $wpdb; $product_id = $wpdb->get_var($wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_woonoow_software_slug' AND meta_value = %s LIMIT 1", $slug )); return $product_id ? wc_get_product($product_id) : null; } /** * Check for updates */ public static function check_update($license_key, $slug, $current_version) { // Validate license $license_validation = LicenseManager::validate($license_key); if (!$license_validation['valid']) { return [ 'success' => false, 'error' => $license_validation['error'] ?? 'invalid_license', 'message' => $license_validation['message'] ?? __('Invalid license key', 'woonoow'), ]; } // Get product by slug $product = self::get_product_by_slug($slug); if (!$product) { return [ 'success' => false, 'error' => 'product_not_found', 'message' => __('Software product not found', 'woonoow'), ]; } $config = self::get_product_config($product->get_id()); if (!$config || !$config['enabled']) { return [ 'success' => false, 'error' => 'software_disabled', 'message' => __('Software distribution is not enabled for this product', 'woonoow'), ]; } $latest_version = $config['current_version']; $update_available = version_compare($current_version, $latest_version, '<'); // Get changelog for latest version $changelog = self::get_version_changelog($product->get_id(), $latest_version); // Build response $response = [ 'success' => true, 'update_available' => $update_available, 'product' => [ 'name' => $product->get_name(), 'slug' => $config['slug'], ], 'current_version' => $current_version, 'latest_version' => $latest_version, 'changelog' => $changelog['changelog'] ?? '', 'release_date' => $changelog['release_date'] ?? null, ]; // Add download URL if update available if ($update_available) { $license = LicenseManager::get_license_by_key($license_key); $token = self::generate_download_token($license['id'], $product->get_id()); $response['download_url'] = rest_url('woonoow/v1/software/download') . '?token=' . $token; $response['changelog_url'] = rest_url('woonoow/v1/software/changelog') . '?slug=' . $config['slug']; } // Add WordPress-specific fields if enabled if ($config['wp_enabled']) { $response['wordpress'] = [ 'requires' => $config['requires_wp'] ?: null, 'tested' => $config['tested_wp'] ?: null, 'requires_php' => $config['requires_php'] ?: null, ]; // Add icons/banners if set if ($config['icon']) { $icon_url = is_numeric($config['icon']) ? wp_get_attachment_url($config['icon']) : $config['icon']; $response['wordpress']['icons'] = [ '1x' => $icon_url, '2x' => $icon_url, ]; } if ($config['banner']) { $banner_url = is_numeric($config['banner']) ? wp_get_attachment_url($config['banner']) : $config['banner']; $response['wordpress']['banners'] = [ 'low' => $banner_url, 'high' => $banner_url, ]; } } return $response; } /** * Get version changelog */ public static function get_version_changelog($product_id, $version = null) { global $wpdb; $table = $wpdb->prefix . self::$versions_table; if ($version) { return $wpdb->get_row($wpdb->prepare( "SELECT version, changelog, release_date FROM $table WHERE product_id = %d AND version = %s", $product_id, $version ), ARRAY_A); } // Get current version return $wpdb->get_row($wpdb->prepare( "SELECT version, changelog, release_date FROM $table WHERE product_id = %d AND is_current = 1", $product_id ), ARRAY_A); } /** * Get all versions for a product */ public static function get_all_versions($product_id) { global $wpdb; $table = $wpdb->prefix . self::$versions_table; return $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table WHERE product_id = %d ORDER BY release_date DESC", $product_id ), ARRAY_A); } /** * Add new version */ public static function add_version($product_id, $version, $changelog, $set_current = true) { global $wpdb; $table = $wpdb->prefix . self::$versions_table; // Check if version already exists $exists = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table WHERE product_id = %d AND version = %s", $product_id, $version )); if ($exists) { return new \WP_Error('version_exists', __('Version already exists', 'woonoow')); } // If setting as current, unset previous current if ($set_current) { $wpdb->update( $table, ['is_current' => 0], ['product_id' => $product_id] ); } // Insert new version $wpdb->insert($table, [ 'product_id' => $product_id, 'version' => $version, 'changelog' => $changelog, 'release_date' => current_time('mysql'), 'is_current' => $set_current ? 1 : 0, ]); // Update product meta if ($set_current) { update_post_meta($product_id, '_woonoow_software_current_version', $version); } do_action('woonoow/software/version_added', $wpdb->insert_id, $product_id, $version); return $wpdb->insert_id; } /** * Generate secure download token */ public static function generate_download_token($license_id, $product_id, $version_id = null) { global $wpdb; $table = $wpdb->prefix . self::$downloads_table; $token = bin2hex(random_bytes(32)); $expires_at = gmdate('Y-m-d H:i:s', time() + 300); // 5 minutes $wpdb->insert($table, [ 'token' => $token, 'license_id' => $license_id, 'product_id' => $product_id, 'version_id' => $version_id, 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null, 'expires_at' => $expires_at, ]); return $token; } /** * Validate and consume download token */ public static function validate_download_token($token) { global $wpdb; $table = $wpdb->prefix . self::$downloads_table; $download = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE token = %s AND used_at IS NULL AND expires_at > %s", $token, current_time('mysql') ), ARRAY_A); if (!$download) { return new \WP_Error('invalid_token', __('Invalid or expired download token', 'woonoow')); } // Mark as used $wpdb->update( $table, ['used_at' => current_time('mysql')], ['id' => $download['id']] ); // Increment download count $versions_table = $wpdb->prefix . self::$versions_table; if ($download['version_id']) { $wpdb->query($wpdb->prepare( "UPDATE $versions_table SET download_count = download_count + 1 WHERE id = %d", $download['version_id'] )); } return $download; } /** * Get downloadable file for product * Uses WooCommerce's existing downloadable files */ public static function get_downloadable_file($product_id) { $product = wc_get_product($product_id); if (!$product || !$product->is_downloadable()) { return null; } $downloads = $product->get_downloads(); if (empty($downloads)) { return null; } // Return first downloadable file $download = reset($downloads); return [ 'id' => $download->get_id(), 'name' => $download->get_name(), 'file' => $download->get_file(), ]; } /** * Serve downloadable file */ public static function serve_file($product_id) { $file_data = self::get_downloadable_file($product_id); if (!$file_data) { wp_die(__('No downloadable file found', 'woonoow'), '', ['response' => 404]); } $file_path = $file_data['file']; // Handle different file types if (strpos($file_path, home_url()) === 0) { // Local file - convert URL to path $upload_dir = wp_upload_dir(); $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_path); } if (!file_exists($file_path)) { // Try as attachment $file_path = get_attached_file(attachment_url_to_postid($file_data['file'])); } if (!$file_path || !file_exists($file_path)) { wp_die(__('File not found', 'woonoow'), '', ['response' => 404]); } // Serve file $filename = basename($file_path); $mime_type = mime_content_type($file_path) ?: 'application/octet-stream'; header('Content-Type: ' . $mime_type); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . filesize($file_path)); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); readfile($file_path); exit; } /** * Clean up expired tokens */ public static function cleanup_expired_tokens() { global $wpdb; $table = $wpdb->prefix . self::$downloads_table; $wpdb->query($wpdb->prepare( "DELETE FROM $table WHERE expires_at < %s", current_time('mysql') )); } }