';
foreach ($items as $item) {
$item_title = esc_html($item['title'] ?? '');
$item_desc = esc_html($item['description'] ?? '');
$item_icon = esc_html($item['icon'] ?? '');
-
+
// Allow overriding item specific style if needed, but for now global
$html .= "
";
-
+
// Render Icon SVG
if ($item_icon) {
$icon_svg = self::get_icon_svg($item_icon);
@@ -353,7 +383,7 @@ class PageSSR
$html .= "
{$icon_svg}
";
}
}
-
+
if ($item_title) {
// Feature title style
$f_title_style = self::generate_style_attr($element_styles['feature_title'] ?? []);
@@ -368,10 +398,10 @@ class PageSSR
}
$html .= '
';
$html .= '';
-
+
return $html;
}
-
+
/**
* Render CTA Banner section
*/
@@ -381,10 +411,10 @@ class PageSSR
$text = esc_html($props['text'] ?? '');
$button_text = esc_html($props['button_text'] ?? '');
$button_url = esc_url($props['button_url'] ?? '');
-
+
$html = "
";
$html .= '';
-
+
if ($title) {
$html .= "
{$title} ";
}
@@ -394,13 +424,13 @@ class PageSSR
if ($button_text && $button_url) {
$html .= "
{$button_text} ";
}
-
+
$html .= '
';
$html .= ' ';
-
+
return $html;
}
-
+
/**
* Render Contact Form section
*/
@@ -410,13 +440,13 @@ class PageSSR
$webhook_url = esc_url($props['webhook_url'] ?? '');
$redirect_url = esc_url($props['redirect_url'] ?? '');
$fields = $props['fields'] ?? ['name', 'email', 'message'];
-
+
// Extract styles
$btn_bg = $element_styles['button']['backgroundColor'] ?? '';
$btn_color = $element_styles['button']['color'] ?? '';
$field_bg = $element_styles['fields']['backgroundColor'] ?? '';
$field_color = $element_styles['fields']['color'] ?? '';
-
+
$btn_style = "";
if ($btn_bg) $btn_style .= "background-color: {$btn_bg};";
if ($btn_color) $btn_style .= "color: {$btn_color};";
@@ -428,14 +458,14 @@ class PageSSR
$field_attr = $field_style ? "style=\"{$field_style}\"" : "";
$html = "
";
-
+
if ($title) {
$html .= "";
}
-
+
// Form is rendered but won't work for bots (they just see the structure)
$html .= '';
$html .= ' ';
-
+
return $html;
}
/**
* Helper to get SVG for known icons
*/
- private static function get_icon_svg($name) {
+ private static function get_icon_svg($name)
+ {
$icons = [
'Star' => '
',
'Zap' => '
',
@@ -473,7 +504,7 @@ class PageSSR
return $icons[$name] ?? $icons['Star'];
}
-
+
/**
* Generic section fallback
*/
@@ -485,7 +516,7 @@ class PageSSR
$content .= "
" . wp_kses_post($value) . "
";
}
}
-
+
return "
";
}
}
diff --git a/includes/Modules/Software/SoftwareManager.php b/includes/Modules/Software/SoftwareManager.php
new file mode 100644
index 0000000..95a640e
--- /dev/null
+++ b/includes/Modules/Software/SoftwareManager.php
@@ -0,0 +1,456 @@
+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')
+ ));
+ }
+}
diff --git a/includes/Modules/Software/SoftwareModule.php b/includes/Modules/Software/SoftwareModule.php
new file mode 100644
index 0000000..931f052
--- /dev/null
+++ b/includes/Modules/Software/SoftwareModule.php
@@ -0,0 +1,200 @@
+prefix . 'woonoow_software_versions';
+
+ if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
+ SoftwareManager::create_tables();
+ }
+ }
+
+ /**
+ * Handle module enable
+ */
+ public static function on_module_enabled($module_id)
+ {
+ if ($module_id === 'software') {
+ SoftwareManager::create_tables();
+ }
+ }
+
+ /**
+ * Add software distribution fields to product edit page
+ */
+ public static function add_product_software_fields()
+ {
+ global $post;
+
+ if (!ModuleRegistry::is_enabled('software')) {
+ return;
+ }
+
+ // Check if licensing is enabled for this product
+ $licensing_enabled = get_post_meta($post->ID, '_woonoow_licensing_enabled', true) === 'yes';
+
+ echo '
';
+
+ // Software Distribution section header
+ echo '
' . esc_html__('Software Distribution', 'woonoow') . '
';
+
+ woocommerce_wp_checkbox([
+ 'id' => '_woonoow_software_enabled',
+ 'label' => __('Enable Software Updates', 'woonoow'),
+ 'description' => __('Allow customers to check for updates via API', 'woonoow'),
+ ]);
+
+ woocommerce_wp_text_input([
+ 'id' => '_woonoow_software_slug',
+ 'label' => __('Software Slug', 'woonoow'),
+ 'description' => __('Unique identifier (e.g., "my-plugin"). Used in update check API.', 'woonoow'),
+ 'desc_tip' => true,
+ 'placeholder' => 'my-software',
+ ]);
+
+ woocommerce_wp_text_input([
+ 'id' => '_woonoow_software_current_version',
+ 'label' => __('Current Version', 'woonoow'),
+ 'description' => __('Latest version number (e.g., "1.2.3")', 'woonoow'),
+ 'desc_tip' => true,
+ 'placeholder' => '1.0.0',
+ ]);
+
+ // WordPress Integration section
+ echo '
' . esc_html__('WordPress Integration (Optional)', 'woonoow') . '
';
+
+ woocommerce_wp_checkbox([
+ 'id' => '_woonoow_software_wp_enabled',
+ 'label' => __('WordPress Plugin/Theme', 'woonoow'),
+ 'description' => __('Enable WordPress-specific update fields', 'woonoow'),
+ ]);
+
+ // WordPress-specific fields (shown via JS when checkbox is checked)
+ echo '
';
+
+ woocommerce_wp_text_input([
+ 'id' => '_woonoow_software_requires_wp',
+ 'label' => __('Requires WP', 'woonoow'),
+ 'placeholder' => '6.0',
+ ]);
+
+ woocommerce_wp_text_input([
+ 'id' => '_woonoow_software_tested_wp',
+ 'label' => __('Tested WP', 'woonoow'),
+ 'placeholder' => '6.7',
+ ]);
+
+ woocommerce_wp_text_input([
+ 'id' => '_woonoow_software_requires_php',
+ 'label' => __('Requires PHP', 'woonoow'),
+ 'placeholder' => '7.4',
+ ]);
+
+ echo '
'; // .woonoow-wp-fields
+
+ // Inline JS to toggle WP fields
+?>
+
+'; // .options_group
+ }
+
+ /**
+ * Save software distribution fields
+ */
+ public static function save_product_software_fields($post_id)
+ {
+ // Software enabled
+ $software_enabled = isset($_POST['_woonoow_software_enabled']) ? 'yes' : 'no';
+ update_post_meta($post_id, '_woonoow_software_enabled', $software_enabled);
+
+ // Software slug
+ if (isset($_POST['_woonoow_software_slug'])) {
+ $slug = sanitize_title($_POST['_woonoow_software_slug']);
+ update_post_meta($post_id, '_woonoow_software_slug', $slug);
+ }
+
+ // Current version
+ if (isset($_POST['_woonoow_software_current_version'])) {
+ $version = sanitize_text_field($_POST['_woonoow_software_current_version']);
+ update_post_meta($post_id, '_woonoow_software_current_version', $version);
+ }
+
+ // WordPress integration
+ $wp_enabled = isset($_POST['_woonoow_software_wp_enabled']) ? 'yes' : 'no';
+ update_post_meta($post_id, '_woonoow_software_wp_enabled', $wp_enabled);
+
+ // WordPress-specific fields
+ $wp_fields = [
+ '_woonoow_software_requires_wp',
+ '_woonoow_software_tested_wp',
+ '_woonoow_software_requires_php',
+ ];
+
+ foreach ($wp_fields as $field) {
+ if (isset($_POST[$field])) {
+ update_post_meta($post_id, $field, sanitize_text_field($_POST[$field]));
+ }
+ }
+ }
+}
diff --git a/includes/Modules/SoftwareSettings.php b/includes/Modules/SoftwareSettings.php
new file mode 100644
index 0000000..f86782a
--- /dev/null
+++ b/includes/Modules/SoftwareSettings.php
@@ -0,0 +1,113 @@
+ 'software',
+ 'name' => __('Software Distribution', 'woonoow'),
+ 'description' => __('Sell and distribute software with version tracking, changelogs, and automatic update checking. Works with any software type.', 'woonoow'),
+ 'icon' => 'Package',
+ 'category' => 'sales',
+ 'requires' => ['licensing'], // Depends on licensing module
+ 'settings' => self::get_settings_schema(),
+ ];
+
+ return $modules;
+ }
+
+ /**
+ * Get settings schema
+ */
+ public static function get_settings_schema()
+ {
+ return [
+ [
+ 'id' => 'rate_limit',
+ 'type' => 'number',
+ 'label' => __('API Rate Limit', 'woonoow'),
+ 'description' => __('Maximum update check requests per minute per license', 'woonoow'),
+ 'default' => 10,
+ 'min' => 1,
+ 'max' => 100,
+ ],
+ [
+ 'id' => 'token_expiry',
+ 'type' => 'number',
+ 'label' => __('Download Token Expiry', 'woonoow'),
+ 'description' => __('Minutes until download token expires (default: 5)', 'woonoow'),
+ 'default' => 5,
+ 'min' => 1,
+ 'max' => 60,
+ ],
+ [
+ 'id' => 'cache_ttl',
+ 'type' => 'number',
+ 'label' => __('Client Cache TTL', 'woonoow'),
+ 'description' => __('Hours to cache update check results on client (default: 12)', 'woonoow'),
+ 'default' => 12,
+ 'min' => 1,
+ 'max' => 168,
+ ],
+ ];
+ }
+
+ /**
+ * Get current settings
+ */
+ public static function get_settings()
+ {
+ $defaults = [
+ 'rate_limit' => 10,
+ 'token_expiry' => 5,
+ 'cache_ttl' => 12,
+ ];
+
+ $settings = get_option(self::$option_key, []);
+
+ return wp_parse_args($settings, $defaults);
+ }
+
+ /**
+ * Save settings
+ */
+ public static function save_settings($settings)
+ {
+ $sanitized = [
+ 'rate_limit' => absint($settings['rate_limit'] ?? 10),
+ 'token_expiry' => absint($settings['token_expiry'] ?? 5),
+ 'cache_ttl' => absint($settings['cache_ttl'] ?? 12),
+ ];
+
+ update_option(self::$option_key, $sanitized);
+
+ return $sanitized;
+ }
+}
diff --git a/templates/updater/class-woonoow-updater.php b/templates/updater/class-woonoow-updater.php
new file mode 100644
index 0000000..37d7661
--- /dev/null
+++ b/templates/updater/class-woonoow-updater.php
@@ -0,0 +1,391 @@
+ 'https://your-store.com/',
+ * 'slug' => 'my-plugin',
+ * 'version' => MY_PLUGIN_VERSION,
+ * 'license_key' => get_option('my_plugin_license_key'),
+ * 'plugin_file' => __FILE__, // For plugins
+ * // OR
+ * 'theme_slug' => 'my-theme', // For themes
+ * ]);
+ */
+
+if (!class_exists('WooNooW_Updater')) {
+
+ class WooNooW_Updater
+ {
+ /**
+ * @var string API base URL
+ */
+ private $api_url;
+
+ /**
+ * @var string Software slug
+ */
+ private $slug;
+
+ /**
+ * @var string Plugin file path (for plugins)
+ */
+ private $plugin_file;
+
+ /**
+ * @var string Theme slug (for themes)
+ */
+ private $theme_slug;
+
+ /**
+ * @var string Current version
+ */
+ private $version;
+
+ /**
+ * @var string License key
+ */
+ private $license_key;
+
+ /**
+ * @var string Cache key for transient
+ */
+ private $cache_key;
+
+ /**
+ * @var int Cache TTL in seconds (default: 12 hours)
+ */
+ private $cache_ttl = 43200;
+
+ /**
+ * @var bool Is this a theme update?
+ */
+ private $is_theme = false;
+
+ /**
+ * Initialize the updater
+ *
+ * @param array $config Configuration array
+ */
+ public function __construct($config)
+ {
+ $this->api_url = trailingslashit($config['api_url'] ?? '');
+ $this->slug = $config['slug'] ?? '';
+ $this->version = $config['version'] ?? '1.0.0';
+ $this->license_key = $config['license_key'] ?? '';
+ $this->cache_key = 'woonoow_update_' . md5($this->slug);
+
+ // Determine if plugin or theme
+ if (!empty($config['theme_slug'])) {
+ $this->is_theme = true;
+ $this->theme_slug = $config['theme_slug'];
+ } else {
+ $this->plugin_file = $config['plugin_file'] ?? '';
+ }
+
+ // Set cache TTL if provided
+ if (!empty($config['cache_ttl'])) {
+ $this->cache_ttl = (int) $config['cache_ttl'] * 3600; // Convert hours to seconds
+ }
+
+ // Don't proceed if no API URL or license
+ if (empty($this->api_url) || empty($this->slug)) {
+ return;
+ }
+
+ // Hook into WordPress update system
+ if ($this->is_theme) {
+ add_filter('pre_set_site_transient_update_themes', [$this, 'check_theme_update']);
+ add_filter('themes_api', [$this, 'theme_info'], 20, 3);
+ } else {
+ add_filter('pre_set_site_transient_update_plugins', [$this, 'check_plugin_update']);
+ add_filter('plugins_api', [$this, 'plugin_info'], 20, 3);
+ }
+
+ // Clear cache on upgrade
+ add_action('upgrader_process_complete', [$this, 'clear_cache'], 10, 2);
+ }
+
+ /**
+ * Check for plugin updates
+ *
+ * @param object $transient Update transient
+ * @return object Modified transient
+ */
+ public function check_plugin_update($transient)
+ {
+ if (empty($transient->checked)) {
+ return $transient;
+ }
+
+ $remote = $this->get_remote_info();
+
+ if ($remote && !empty($remote->latest_version)) {
+ if (version_compare($this->version, $remote->latest_version, '<')) {
+ $plugin_file = plugin_basename($this->plugin_file);
+
+ $transient->response[$plugin_file] = (object) [
+ 'slug' => $this->slug,
+ 'plugin' => $plugin_file,
+ 'new_version' => $remote->latest_version,
+ 'package' => $remote->download_url ?? '',
+ 'url' => $remote->homepage ?? '',
+ 'tested' => $remote->wordpress->tested ?? '',
+ 'requires' => $remote->wordpress->requires ?? '',
+ 'requires_php' => $remote->wordpress->requires_php ?? '',
+ 'icons' => (array) ($remote->wordpress->icons ?? []),
+ 'banners' => (array) ($remote->wordpress->banners ?? []),
+ ];
+ } else {
+ // No update available
+ $transient->no_update[plugin_basename($this->plugin_file)] = (object) [
+ 'slug' => $this->slug,
+ 'plugin' => plugin_basename($this->plugin_file),
+ 'new_version' => $this->version,
+ ];
+ }
+ }
+
+ return $transient;
+ }
+
+ /**
+ * Check for theme updates
+ *
+ * @param object $transient Update transient
+ * @return object Modified transient
+ */
+ public function check_theme_update($transient)
+ {
+ if (empty($transient->checked)) {
+ return $transient;
+ }
+
+ $remote = $this->get_remote_info();
+
+ if ($remote && !empty($remote->latest_version)) {
+ if (version_compare($this->version, $remote->latest_version, '<')) {
+ $transient->response[$this->theme_slug] = [
+ 'theme' => $this->theme_slug,
+ 'new_version' => $remote->latest_version,
+ 'package' => $remote->download_url ?? '',
+ 'url' => $remote->homepage ?? '',
+ 'requires' => $remote->wordpress->requires ?? '',
+ 'requires_php' => $remote->wordpress->requires_php ?? '',
+ ];
+ }
+ }
+
+ return $transient;
+ }
+
+ /**
+ * Provide plugin information for details popup
+ *
+ * @param mixed $result Default result
+ * @param string $action Action being performed
+ * @param object $args Arguments
+ * @return mixed Plugin info or default result
+ */
+ public function plugin_info($result, $action, $args)
+ {
+ if ($action !== 'plugin_information' || !isset($args->slug) || $args->slug !== $this->slug) {
+ return $result;
+ }
+
+ $remote = $this->get_remote_info();
+
+ if (!$remote) {
+ return $result;
+ }
+
+ return (object) [
+ 'name' => $remote->product->name ?? $this->slug,
+ 'slug' => $this->slug,
+ 'version' => $remote->latest_version,
+ 'tested' => $remote->wordpress->tested ?? '',
+ 'requires' => $remote->wordpress->requires ?? '',
+ 'requires_php' => $remote->wordpress->requires_php ?? '',
+ 'author' => $remote->author ?? '',
+ 'homepage' => $remote->homepage ?? '',
+ 'download_link' => $remote->download_url ?? '',
+ 'sections' => [
+ 'description' => $remote->description ?? '',
+ 'changelog' => nl2br($remote->changelog ?? ''),
+ ],
+ 'banners' => (array) ($remote->wordpress->banners ?? []),
+ 'icons' => (array) ($remote->wordpress->icons ?? []),
+ 'last_updated' => $remote->release_date ?? '',
+ ];
+ }
+
+ /**
+ * Provide theme information for details popup
+ *
+ * @param mixed $result Default result
+ * @param string $action Action being performed
+ * @param object $args Arguments
+ * @return mixed Theme info or default result
+ */
+ public function theme_info($result, $action, $args)
+ {
+ if ($action !== 'theme_information' || !isset($args->slug) || $args->slug !== $this->theme_slug) {
+ return $result;
+ }
+
+ $remote = $this->get_remote_info();
+
+ if (!$remote) {
+ return $result;
+ }
+
+ return (object) [
+ 'name' => $remote->product->name ?? $this->theme_slug,
+ 'slug' => $this->theme_slug,
+ 'version' => $remote->latest_version,
+ 'requires' => $remote->wordpress->requires ?? '',
+ 'requires_php' => $remote->wordpress->requires_php ?? '',
+ 'author' => $remote->author ?? '',
+ 'homepage' => $remote->homepage ?? '',
+ 'download_link' => $remote->download_url ?? '',
+ 'sections' => [
+ 'description' => $remote->description ?? '',
+ 'changelog' => nl2br($remote->changelog ?? ''),
+ ],
+ 'last_updated' => $remote->release_date ?? '',
+ ];
+ }
+
+ /**
+ * Get remote version info from API
+ *
+ * @return object|null Remote info or null on failure
+ */
+ private function get_remote_info()
+ {
+ // Check cache first
+ $cached = get_transient($this->cache_key);
+ if ($cached !== false) {
+ if ($cached === 'no_update') {
+ return null;
+ }
+ return $cached;
+ }
+
+ // Make API request
+ $response = wp_remote_post($this->api_url . 'wp-json/woonoow/v1/software/check', [
+ 'timeout' => 15,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => wp_json_encode([
+ 'license_key' => $this->license_key,
+ 'slug' => $this->slug,
+ 'version' => $this->version,
+ 'site_url' => home_url(),
+ 'wp_version' => get_bloginfo('version'),
+ 'php_version' => phpversion(),
+ ]),
+ ]);
+
+ if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
+ // Cache failure for shorter time to retry sooner
+ set_transient($this->cache_key, 'no_update', 3600);
+ return null;
+ }
+
+ $data = json_decode(wp_remote_retrieve_body($response));
+
+ if (!$data || empty($data->success)) {
+ set_transient($this->cache_key, 'no_update', 3600);
+ return null;
+ }
+
+ if (empty($data->update_available)) {
+ set_transient($this->cache_key, 'no_update', $this->cache_ttl);
+ return null;
+ }
+
+ // Cache the update info
+ set_transient($this->cache_key, $data, $this->cache_ttl);
+
+ return $data;
+ }
+
+ /**
+ * Clear update cache after upgrade
+ *
+ * @param object $upgrader Upgrader instance
+ * @param array $options Upgrade options
+ */
+ public function clear_cache($upgrader, $options)
+ {
+ if ($this->is_theme) {
+ if ($options['action'] === 'update' && $options['type'] === 'theme') {
+ if (isset($options['themes']) && in_array($this->theme_slug, $options['themes'])) {
+ delete_transient($this->cache_key);
+ }
+ }
+ } else {
+ if ($options['action'] === 'update' && $options['type'] === 'plugin') {
+ if (isset($options['plugins']) && in_array(plugin_basename($this->plugin_file), $options['plugins'])) {
+ delete_transient($this->cache_key);
+ }
+ }
+ }
+ }
+
+ /**
+ * Force check for updates (bypass cache)
+ */
+ public function force_check()
+ {
+ delete_transient($this->cache_key);
+ return $this->get_remote_info();
+ }
+
+ /**
+ * Get license status
+ *
+ * @return array|null License status or null on failure
+ */
+ public function get_license_status()
+ {
+ if (empty($this->license_key)) {
+ return ['valid' => false, 'error' => 'no_license'];
+ }
+
+ $response = wp_remote_post($this->api_url . 'wp-json/woonoow/v1/licenses/validate', [
+ 'timeout' => 15,
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => wp_json_encode([
+ 'license_key' => $this->license_key,
+ ]),
+ ]);
+
+ if (is_wp_error($response)) {
+ return null;
+ }
+
+ return json_decode(wp_remote_retrieve_body($response), true);
+ }
+ }
+}
diff --git a/woonoow.php b/woonoow.php
index 845eb4e..0235af1 100644
--- a/woonoow.php
+++ b/woonoow.php
@@ -1,4 +1,5 @@