Adding more functionality

This commit is contained in:
2025-08-29 18:54:14 -07:00
parent 5aa0777fd3
commit 6d797ef686
17 changed files with 4237 additions and 85 deletions

785
includes/class-wpdd-api.php Normal file
View File

@@ -0,0 +1,785 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_API {
/**
* Initialize the API endpoints
*/
public static function init() {
add_action('rest_api_init', array(__CLASS__, 'register_routes'));
}
/**
* Register REST API routes
*/
public static function register_routes() {
// License validation
register_rest_route('wpdd/v1', '/validate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'validate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'product_slug' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => false,
'sanitize_callback' => 'esc_url_raw'
)
)
));
// License activation
register_rest_route('wpdd/v1', '/activate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'activate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => true,
'sanitize_callback' => 'esc_url_raw'
),
'site_name' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'wp_version' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
),
'php_version' => array(
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// License deactivation
register_rest_route('wpdd/v1', '/deactivate-license', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'deactivate_license'),
'permission_callback' => '__return_true',
'args' => array(
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'site_url' => array(
'required' => true,
'sanitize_callback' => 'esc_url_raw'
)
)
));
// Check for updates
register_rest_route('wpdd/v1', '/check-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'check_update'),
'permission_callback' => '__return_true',
'args' => array(
'product_slug' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'version' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// Download update
register_rest_route('wpdd/v1', '/download-update/(?P<product_slug>[a-zA-Z0-9-]+)', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'download_update'),
'permission_callback' => '__return_true',
'args' => array(
'product_slug' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
),
'license_key' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
// Webhook endpoint with secure passcode
register_rest_route('wpdd/v1', '/webhook/(?P<product_id>\d+)/(?P<passcode>[a-zA-Z0-9]+)', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'handle_webhook'),
'permission_callback' => '__return_true',
'args' => array(
'product_id' => array(
'required' => true,
'sanitize_callback' => 'absint'
),
'passcode' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
)
)
));
}
/**
* Validate license endpoint
*/
public static function validate_license($request) {
$license_key = $request->get_param('license_key');
$product_slug = $request->get_param('product_slug');
$site_url = $request->get_param('site_url');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::validate_license($license_key, $product_slug, $site_url);
if ($result['valid']) {
return new WP_REST_Response(array(
'success' => true,
'message' => $result['message'],
'license' => array(
'status' => $result['license']->status,
'expires_at' => $result['license']->expires_at,
'activations' => $result['license']->activations_count,
'max_activations' => $result['license']->max_activations
)
), 200);
} else {
return new WP_REST_Response(array(
'success' => false,
'error' => $result['error'],
'message' => $result['message']
), 400);
}
}
/**
* Activate license endpoint
*/
public static function activate_license($request) {
$license_key = $request->get_param('license_key');
$site_url = $request->get_param('site_url');
$site_name = $request->get_param('site_name');
$wp_version = $request->get_param('wp_version');
$php_version = $request->get_param('php_version');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::activate_license($license_key, $site_url, $site_name, $wp_version, $php_version);
if ($result['success']) {
return new WP_REST_Response($result, 200);
} else {
return new WP_REST_Response($result, 400);
}
}
/**
* Deactivate license endpoint
*/
public static function deactivate_license($request) {
$license_key = $request->get_param('license_key');
$site_url = $request->get_param('site_url');
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$result = WPDD_License_Manager::deactivate_license($license_key, $site_url);
if ($result['success']) {
return new WP_REST_Response($result, 200);
} else {
return new WP_REST_Response($result, 400);
}
}
/**
* Check for updates endpoint
*/
public static function check_update($request) {
global $wpdb;
$product_slug = $request->get_param('product_slug');
$license_key = $request->get_param('license_key');
$current_version = $request->get_param('version');
// Validate license first
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
if (!$validation['valid']) {
return new WP_REST_Response(array(
'success' => false,
'error' => $validation['error'],
'message' => $validation['message']
), 403);
}
// Get product by slug
$product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
if (!$product) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Product not found.', 'wp-digital-download')
), 404);
}
// Get latest version
$latest_version = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_software_versions
WHERE product_id = %d
ORDER BY release_date DESC
LIMIT 1",
$product->ID
));
if (!$latest_version) {
return new WP_REST_Response(array(
'success' => true,
'update_available' => false,
'message' => __('No updates available.', 'wp-digital-download')
), 200);
}
// Compare versions
if (version_compare($latest_version->version, $current_version, '>')) {
// Update available
$update_data = array(
'success' => true,
'update_available' => true,
'version' => $latest_version->version,
'download_url' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
'package' => home_url("/wp-json/wpdd/v1/download-update/{$product_slug}?license_key={$license_key}"),
'url' => get_permalink($product->ID),
'tested' => $latest_version->tested_wp_version ?: get_bloginfo('version'),
'requires' => $latest_version->min_wp_version ?: '5.0',
'requires_php' => $latest_version->min_php_version ?: '7.0',
'new_version' => $latest_version->version,
'slug' => $product_slug,
'plugin' => $product_slug . '/' . $product_slug . '.php', // Adjust based on your naming convention
'changelog' => $latest_version->changelog,
'release_notes' => $latest_version->release_notes
);
return new WP_REST_Response($update_data, 200);
} else {
return new WP_REST_Response(array(
'success' => true,
'update_available' => false,
'message' => __('You have the latest version.', 'wp-digital-download')
), 200);
}
}
/**
* Download update endpoint
*/
public static function download_update($request) {
global $wpdb;
$product_slug = $request->get_param('product_slug');
$license_key = $request->get_param('license_key');
// Validate license
if (!class_exists('WPDD_License_Manager')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-license-manager.php';
}
$validation = WPDD_License_Manager::validate_license($license_key, $product_slug);
if (!$validation['valid']) {
return new WP_REST_Response(array(
'success' => false,
'error' => $validation['error'],
'message' => $validation['message']
), 403);
}
// Get product
$product = get_page_by_path($product_slug, OBJECT, 'wpdd_product');
if (!$product) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Product not found.', 'wp-digital-download')
), 404);
}
// Get latest version
$latest_version = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_software_versions
WHERE product_id = %d
ORDER BY release_date DESC
LIMIT 1",
$product->ID
));
if (!$latest_version || !$latest_version->package_url) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Update package not available.', 'wp-digital-download')
), 404);
}
// Get package file path
$upload_dir = wp_upload_dir();
$package_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $latest_version->package_url);
if (!file_exists($package_path)) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Update package file not found.', 'wp-digital-download')
), 404);
}
// Log download
$wpdb->insert(
$wpdb->prefix . 'wpdd_downloads',
array(
'order_id' => $validation['license']->order_id,
'product_id' => $product->ID,
'customer_id' => $validation['license']->customer_id,
'file_id' => $latest_version->version,
'download_date' => current_time('mysql'),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT']
),
array('%d', '%d', '%d', '%s', '%s', '%s', '%s')
);
// Serve file
$filename = basename($package_path);
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($package_path));
header('Pragma: no-cache');
header('Expires: 0');
readfile($package_path);
exit;
}
/**
* Handle Git webhook for new releases (receives notifications FROM Git platforms like Gitea)
*/
public static function handle_webhook($request) {
global $wpdb;
$product_id = $request->get_param('product_id');
$passcode = $request->get_param('passcode');
// Validate passcode
$stored_passcode = get_post_meta($product_id, '_wpdd_webhook_passcode', true);
if (!$stored_passcode || $stored_passcode !== $passcode) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Invalid webhook passcode.', 'wp-digital-download')
), 403);
}
// Get payload from Git platform (Gitea, GitHub, GitLab, etc.)
$payload = $request->get_body();
$data = json_decode($payload, true);
if (!$data) {
return new WP_REST_Response(array(
'success' => false,
'message' => __('Invalid JSON payload.', 'wp-digital-download')
), 400);
}
// Determine event type based on payload structure
$event_type = 'unknown';
$is_release = false;
// Gitea release webhook
if (isset($data['action']) && isset($data['release'])) {
$event_type = 'release';
$is_release = ($data['action'] === 'published' || $data['action'] === 'created');
}
// GitHub/GitLab push with tags
elseif (isset($data['ref']) && strpos($data['ref'], 'refs/tags/') === 0) {
$event_type = 'tag_push';
$is_release = true;
}
// GitHub release webhook
elseif (isset($data['action']) && isset($data['release']) && $data['action'] === 'published') {
$event_type = 'github_release';
$is_release = true;
}
// Log webhook event
$wpdb->insert(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'product_id' => $product_id,
'event_type' => $event_type,
'payload' => $payload,
'processed' => 'pending',
'received_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%s', '%s')
);
$event_id = $wpdb->insert_id;
if (!$is_release) {
// Mark as ignored - not a release event
$wpdb->update(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'processed' => 'ignored',
'processed_at' => current_time('mysql'),
'error_message' => 'Not a release event'
),
array('id' => $event_id),
array('%s', '%s', '%s'),
array('%d')
);
return new WP_REST_Response(array(
'success' => true,
'message' => __('Webhook received but not a release event.', 'wp-digital-download')
), 200);
}
// Extract version information based on platform
$version = '';
$tag = '';
if ($event_type === 'release' || $event_type === 'github_release') {
// Gitea or GitHub release
$tag = $data['release']['tag_name'] ?? '';
$version = ltrim($tag, 'v');
} elseif ($event_type === 'tag_push') {
// Git tag push
$tag = str_replace('refs/tags/', '', $data['ref']);
$version = ltrim($tag, 'v');
}
if (empty($version)) {
// Mark as error
$wpdb->update(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'processed' => 'error',
'processed_at' => current_time('mysql'),
'error_message' => 'Could not extract version from payload'
),
array('id' => $event_id),
array('%s', '%s', '%s'),
array('%d')
);
return new WP_REST_Response(array(
'success' => false,
'message' => __('Could not extract version from webhook payload.', 'wp-digital-download')
), 400);
}
// Check if this version already exists
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wpdd_software_versions
WHERE product_id = %d AND version = %s",
$product_id,
$version
));
if (!$existing) {
// Process new release
$success = self::process_new_release($product_id, $version, $tag, $data);
if ($success) {
// Mark webhook as processed
$wpdb->update(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'processed' => 'completed',
'processed_at' => current_time('mysql')
),
array('id' => $event_id),
array('%s', '%s'),
array('%d')
);
} else {
// Mark as error
$wpdb->update(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'processed' => 'error',
'processed_at' => current_time('mysql'),
'error_message' => 'Failed to process release'
),
array('id' => $event_id),
array('%s', '%s', '%s'),
array('%d')
);
}
} else {
// Mark as duplicate
$wpdb->update(
$wpdb->prefix . 'wpdd_webhook_events',
array(
'processed' => 'duplicate',
'processed_at' => current_time('mysql'),
'error_message' => 'Version already exists'
),
array('id' => $event_id),
array('%s', '%s', '%s'),
array('%d')
);
}
return new WP_REST_Response(array(
'success' => true,
'message' => __('Webhook received and processed.', 'wp-digital-download')
), 200);
}
/**
* Process new release from webhook (receives data FROM Git platforms like Gitea)
*/
private static function process_new_release($product_id, $version, $tag, $webhook_data) {
global $wpdb;
// Get Git repository settings
$git_url = get_post_meta($product_id, '_wpdd_git_repository', true);
$git_username = get_post_meta($product_id, '_wpdd_git_username', true);
$git_token = get_post_meta($product_id, '_wpdd_git_token', true);
if (!$git_url) {
error_log('WPDD: No Git URL configured for product ' . $product_id);
return false;
}
// Build package from Git repository at the specific tag
$package_url = self::build_package_from_git($product_id, $git_url, $tag, $git_username, $git_token);
if (!$package_url) {
error_log('WPDD: Failed to build package for product ' . $product_id . ' version ' . $version);
return false;
}
// Extract changelog based on webhook source
$changelog = '';
$git_commit = null;
// Gitea/GitHub release with description
if (isset($webhook_data['release'])) {
$changelog = $webhook_data['release']['body'] ?? $webhook_data['release']['note'] ?? '';
$git_commit = $webhook_data['release']['target_commitish'] ?? null;
}
// Git push webhook - use commit messages
elseif (isset($webhook_data['commits']) && is_array($webhook_data['commits'])) {
$messages = array();
foreach ($webhook_data['commits'] as $commit) {
if (isset($commit['message'])) {
$messages[] = '- ' . $commit['message'];
}
}
$changelog = implode("\n", $messages);
$git_commit = $webhook_data['after'] ?? $webhook_data['head_commit']['id'] ?? null;
}
// Fallback - try to get from head commit
elseif (isset($webhook_data['head_commit']['message'])) {
$changelog = '- ' . $webhook_data['head_commit']['message'];
$git_commit = $webhook_data['head_commit']['id'] ?? null;
}
// Insert new version
$result = $wpdb->insert(
$wpdb->prefix . 'wpdd_software_versions',
array(
'product_id' => $product_id,
'version' => $version,
'changelog' => $changelog,
'package_url' => $package_url,
'git_tag' => $tag,
'git_commit' => $git_commit,
'release_date' => current_time('mysql')
),
array('%d', '%s', '%s', '%s', '%s', '%s', '%s')
);
if ($result === false) {
error_log('WPDD: Failed to insert version record for product ' . $product_id . ' version ' . $version);
return false;
}
// Update product version meta
update_post_meta($product_id, '_wpdd_current_version', $version);
// Notify customers about update (optional)
self::notify_customers_about_update($product_id, $version);
error_log('WPDD: Successfully processed new release for product ' . $product_id . ' version ' . $version);
return true;
}
/**
* Build package from Git repository at specific tag
*/
private static function build_package_from_git($product_id, $git_url, $tag, $username = null, $token = null) {
$upload_dir = wp_upload_dir();
$package_dir = trailingslashit($upload_dir['basedir']) . 'wpdd-packages/' . $product_id;
if (!file_exists($package_dir)) {
wp_mkdir_p($package_dir);
}
$package_filename = sanitize_file_name("package-{$tag}.zip");
$package_path = trailingslashit($package_dir) . $package_filename;
$package_url = trailingslashit($upload_dir['baseurl']) . 'wpdd-packages/' . $product_id . '/' . $package_filename;
// Skip if package already exists
if (file_exists($package_path)) {
return $package_url;
}
// Create temporary directory for cloning
$temp_dir = trailingslashit(sys_get_temp_dir()) . 'wpdd-build-' . $product_id . '-' . uniqid();
// Build authentication URL if credentials provided
$auth_url = $git_url;
if ($username && $token) {
$parsed_url = parse_url($git_url);
if ($parsed_url) {
$auth_url = $parsed_url['scheme'] . '://' . urlencode($username) . ':' . urlencode($token) . '@' . $parsed_url['host'];
if (isset($parsed_url['port'])) {
$auth_url .= ':' . $parsed_url['port'];
}
$auth_url .= $parsed_url['path'];
}
}
// Clone repository at specific tag
$clone_cmd = sprintf(
'git clone --depth 1 --branch %s %s %s 2>&1',
escapeshellarg($tag),
escapeshellarg($auth_url),
escapeshellarg($temp_dir)
);
$output = array();
$return_code = 0;
exec($clone_cmd, $output, $return_code);
if ($return_code !== 0) {
error_log('WPDD: Git clone failed for ' . $git_url . ' tag ' . $tag . ': ' . implode(' ', $output));
return false;
}
// Remove .git directory and other development files
$cleanup_files = array('.git', '.gitignore', '.gitattributes', 'tests', 'test', '.phpunit.xml', 'composer.json', 'package.json');
foreach ($cleanup_files as $cleanup_file) {
$cleanup_path = trailingslashit($temp_dir) . $cleanup_file;
if (file_exists($cleanup_path)) {
if (is_dir($cleanup_path)) {
self::remove_directory($cleanup_path);
} else {
unlink($cleanup_path);
}
}
}
// Create ZIP package
if (class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($package_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
self::add_directory_to_zip($zip, $temp_dir, '');
$zip->close();
// Clean up temporary directory
self::remove_directory($temp_dir);
return $package_url;
}
}
// Fallback: tar command if ZipArchive not available
$tar_cmd = sprintf(
'cd %s && tar -czf %s . 2>&1',
escapeshellarg($temp_dir),
escapeshellarg($package_path . '.tar.gz')
);
exec($tar_cmd, $output, $return_code);
self::remove_directory($temp_dir);
if ($return_code === 0 && file_exists($package_path . '.tar.gz')) {
return $package_url . '.tar.gz';
}
error_log('WPDD: Failed to create package for ' . $git_url . ' tag ' . $tag);
return false;
}
/**
* Recursively add directory to ZIP archive
*/
private static function add_directory_to_zip($zip, $dir, $base_path) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($iterator as $file) {
if ($file->isFile()) {
$file_path = $file->getPathname();
$relative_path = $base_path . substr($file_path, strlen($dir) + 1);
$zip->addFile($file_path, $relative_path);
}
}
}
/**
* Recursively remove directory
*/
private static function remove_directory($dir) {
if (!is_dir($dir)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($dir);
}
/**
* Notify customers about new update
*/
private static function notify_customers_about_update($product_id, $version) {
// Optional: Send email notifications to customers with active licenses
// This could be a separate scheduled job to avoid timeout issues
}
}

View File

@@ -20,9 +20,13 @@ class WPDD_Install {
WPDD_Post_Types::register_taxonomies();
self::create_tables();
self::create_pages();
self::create_upload_protection();
// Set flag to show setup notice instead of creating pages automatically
if (!get_option('wpdd_setup_completed')) {
add_option('wpdd_show_setup_notice', true);
}
WPDD_Roles::create_roles();
// Flush rewrite rules after post types are registered
@@ -126,6 +130,96 @@ class WPDD_Install {
KEY transaction_id (transaction_id)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_balance_adjustments (
id bigint(20) NOT NULL AUTO_INCREMENT,
creator_id bigint(20) NOT NULL,
adjustment_type varchar(20) NOT NULL,
amount decimal(10,2) NOT NULL,
previous_balance decimal(10,2) NOT NULL,
new_balance decimal(10,2) NOT NULL,
reason text NOT NULL,
adjusted_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY creator_id (creator_id),
KEY adjusted_by (adjusted_by)
) $charset_collate;";
// Software Licensing Tables
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_licenses (
id bigint(20) NOT NULL AUTO_INCREMENT,
license_key varchar(64) NOT NULL,
product_id bigint(20) NOT NULL,
order_id bigint(20) NOT NULL,
customer_id bigint(20) NOT NULL,
customer_email varchar(100) NOT NULL,
status varchar(20) NOT NULL DEFAULT 'active',
activations_count int(11) DEFAULT 0,
max_activations int(11) DEFAULT 1,
expires_at datetime DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
last_checked datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY license_key (license_key),
KEY product_id (product_id),
KEY order_id (order_id),
KEY customer_id (customer_id),
KEY status (status)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_license_activations (
id bigint(20) NOT NULL AUTO_INCREMENT,
license_id bigint(20) NOT NULL,
license_key varchar(64) NOT NULL,
site_url varchar(255) NOT NULL,
site_name varchar(255) DEFAULT NULL,
activated_at datetime DEFAULT CURRENT_TIMESTAMP,
last_checked datetime DEFAULT NULL,
wp_version varchar(20) DEFAULT NULL,
php_version varchar(20) DEFAULT NULL,
status varchar(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
KEY license_id (license_id),
KEY license_key (license_key),
KEY site_url (site_url),
KEY status (status)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_software_versions (
id bigint(20) NOT NULL AUTO_INCREMENT,
product_id bigint(20) NOT NULL,
version varchar(20) NOT NULL,
changelog text DEFAULT NULL,
release_notes text DEFAULT NULL,
download_url text DEFAULT NULL,
package_url text DEFAULT NULL,
min_wp_version varchar(20) DEFAULT NULL,
tested_wp_version varchar(20) DEFAULT NULL,
min_php_version varchar(20) DEFAULT NULL,
release_date datetime DEFAULT CURRENT_TIMESTAMP,
git_tag varchar(100) DEFAULT NULL,
git_commit varchar(100) DEFAULT NULL,
PRIMARY KEY (id),
KEY product_id (product_id),
KEY version (version),
KEY release_date (release_date)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_webhook_events (
id bigint(20) NOT NULL AUTO_INCREMENT,
product_id bigint(20) NOT NULL,
event_type varchar(50) NOT NULL,
payload text DEFAULT NULL,
processed varchar(20) NOT NULL DEFAULT 'pending',
error_message text DEFAULT NULL,
received_at datetime DEFAULT CURRENT_TIMESTAMP,
processed_at datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY product_id (product_id),
KEY processed (processed),
KEY received_at (received_at)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
foreach ($sql as $query) {
@@ -135,6 +229,10 @@ class WPDD_Install {
update_option('wpdd_db_version', WPDD_VERSION);
}
public static function create_pages_optional() {
return self::create_pages();
}
private static function create_pages() {
$pages = array(
'shop' => array(
@@ -159,6 +257,8 @@ class WPDD_Install {
)
);
$created_pages = array();
foreach ($pages as $slug => $page) {
// Check if page already exists
$existing_page_id = get_option($page['option']);
@@ -184,8 +284,11 @@ class WPDD_Install {
if ($page_id && !is_wp_error($page_id)) {
update_option($page['option'], $page_id);
$created_pages[] = $page_id;
}
}
return $created_pages;
}
private static function create_upload_protection() {

View File

@@ -0,0 +1,399 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_License_Manager {
/**
* Initialize the license manager
*/
public static function init() {
add_action('wpdd_order_completed', array(__CLASS__, 'generate_license_for_order'), 10, 2);
}
/**
* Generate a unique license key
* Format: XXXX-XXXX-XXXX-XXXX
*/
public static function generate_license_key() {
$segments = array();
for ($i = 0; $i < 4; $i++) {
$segments[] = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
}
return implode('-', $segments);
}
/**
* Generate license for completed order
*/
public static function generate_license_for_order($order_id, $order) {
global $wpdb;
// Check if product is software license type
$product_type = get_post_meta($order->product_id, '_wpdd_product_type', true);
if ($product_type !== 'software_license') {
return;
}
// Check if license already exists for this order
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}wpdd_licenses WHERE order_id = %d",
$order_id
));
if ($existing) {
return;
}
// Generate unique license key
$license_key = self::generate_license_key();
// Ensure it's unique
while (self::license_key_exists($license_key)) {
$license_key = self::generate_license_key();
}
// Get license settings from product
$max_activations = get_post_meta($order->product_id, '_wpdd_max_activations', true) ?: 1;
$license_duration = get_post_meta($order->product_id, '_wpdd_license_duration', true); // in days
$expires_at = null;
if ($license_duration && $license_duration > 0) {
$expires_at = date('Y-m-d H:i:s', strtotime("+{$license_duration} days"));
}
// Insert license
$wpdb->insert(
$wpdb->prefix . 'wpdd_licenses',
array(
'license_key' => $license_key,
'product_id' => $order->product_id,
'order_id' => $order_id,
'customer_id' => $order->customer_id,
'customer_email' => $order->customer_email,
'status' => 'active',
'max_activations' => $max_activations,
'expires_at' => $expires_at,
'created_at' => current_time('mysql')
),
array('%s', '%d', '%d', '%d', '%s', '%s', '%d', '%s', '%s')
);
// Send license key to customer
self::send_license_email($order, $license_key);
return $license_key;
}
/**
* Check if license key exists
*/
public static function license_key_exists($license_key) {
global $wpdb;
return $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s",
$license_key
)) > 0;
}
/**
* Validate license key
*/
public static function validate_license($license_key, $product_slug = null, $site_url = null) {
global $wpdb;
// Get license details
$license = $wpdb->get_row($wpdb->prepare(
"SELECT l.*, p.post_name as product_slug
FROM {$wpdb->prefix}wpdd_licenses l
LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID
WHERE l.license_key = %s",
$license_key
));
if (!$license) {
return array(
'valid' => false,
'error' => 'invalid_license',
'message' => __('Invalid license key.', 'wp-digital-download')
);
}
// Check product match
if ($product_slug && $license->product_slug !== $product_slug) {
return array(
'valid' => false,
'error' => 'product_mismatch',
'message' => __('License key is not valid for this product.', 'wp-digital-download')
);
}
// Check status
if ($license->status !== 'active') {
return array(
'valid' => false,
'error' => 'license_' . $license->status,
'message' => sprintf(__('License is %s.', 'wp-digital-download'), $license->status)
);
}
// Check expiration
if ($license->expires_at && strtotime($license->expires_at) < time()) {
// Update status to expired
$wpdb->update(
$wpdb->prefix . 'wpdd_licenses',
array('status' => 'expired'),
array('id' => $license->id),
array('%s'),
array('%d')
);
return array(
'valid' => false,
'error' => 'license_expired',
'message' => __('License has expired.', 'wp-digital-download'),
'expired_at' => $license->expires_at
);
}
// Check activation limit if site_url provided
if ($site_url) {
$activation_count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d AND status = 'active'",
$license->id
));
$is_activated = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d AND site_url = %s AND status = 'active'",
$license->id,
$site_url
));
if (!$is_activated && $activation_count >= $license->max_activations) {
return array(
'valid' => false,
'error' => 'activation_limit',
'message' => sprintf(__('License activation limit reached (%d/%d).', 'wp-digital-download'),
$activation_count, $license->max_activations),
'activations' => $activation_count,
'max_activations' => $license->max_activations
);
}
}
// Update last checked
$wpdb->update(
$wpdb->prefix . 'wpdd_licenses',
array('last_checked' => current_time('mysql')),
array('id' => $license->id),
array('%s'),
array('%d')
);
return array(
'valid' => true,
'license' => $license,
'message' => __('License is valid.', 'wp-digital-download')
);
}
/**
* Activate license for a site
*/
public static function activate_license($license_key, $site_url, $site_name = null, $wp_version = null, $php_version = null) {
global $wpdb;
// Validate license first
$validation = self::validate_license($license_key, null, $site_url);
if (!$validation['valid']) {
return $validation;
}
$license = $validation['license'];
// Check if already activated for this site
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d AND site_url = %s",
$license->id,
$site_url
));
if ($existing && $existing->status === 'active') {
return array(
'success' => true,
'already_active' => true,
'message' => __('License already activated for this site.', 'wp-digital-download')
);
}
if ($existing) {
// Reactivate
$wpdb->update(
$wpdb->prefix . 'wpdd_license_activations',
array(
'status' => 'active',
'activated_at' => current_time('mysql'),
'last_checked' => current_time('mysql'),
'wp_version' => $wp_version,
'php_version' => $php_version,
'site_name' => $site_name
),
array('id' => $existing->id),
array('%s', '%s', '%s', '%s', '%s', '%s'),
array('%d')
);
} else {
// New activation
$wpdb->insert(
$wpdb->prefix . 'wpdd_license_activations',
array(
'license_id' => $license->id,
'license_key' => $license_key,
'site_url' => $site_url,
'site_name' => $site_name,
'activated_at' => current_time('mysql'),
'last_checked' => current_time('mysql'),
'wp_version' => $wp_version,
'php_version' => $php_version,
'status' => 'active'
),
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')
);
}
// Update activation count
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d AND status = 'active'",
$license->id
));
$wpdb->update(
$wpdb->prefix . 'wpdd_licenses',
array('activations_count' => $count),
array('id' => $license->id),
array('%d'),
array('%d')
);
return array(
'success' => true,
'message' => __('License activated successfully.', 'wp-digital-download'),
'activations' => $count,
'max_activations' => $license->max_activations
);
}
/**
* Deactivate license for a site
*/
public static function deactivate_license($license_key, $site_url) {
global $wpdb;
// Get license
$license = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_licenses WHERE license_key = %s",
$license_key
));
if (!$license) {
return array(
'success' => false,
'error' => 'invalid_license',
'message' => __('Invalid license key.', 'wp-digital-download')
);
}
// Deactivate
$updated = $wpdb->update(
$wpdb->prefix . 'wpdd_license_activations',
array('status' => 'deactivated'),
array(
'license_id' => $license->id,
'site_url' => $site_url
),
array('%s'),
array('%d', '%s')
);
if (!$updated) {
return array(
'success' => false,
'error' => 'not_activated',
'message' => __('License not activated for this site.', 'wp-digital-download')
);
}
// Update activation count
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d AND status = 'active'",
$license->id
));
$wpdb->update(
$wpdb->prefix . 'wpdd_licenses',
array('activations_count' => $count),
array('id' => $license->id),
array('%d'),
array('%d')
);
return array(
'success' => true,
'message' => __('License deactivated successfully.', 'wp-digital-download')
);
}
/**
* Send license email to customer
*/
private static function send_license_email($order, $license_key) {
$product = get_post($order->product_id);
$subject = sprintf(__('Your License Key for %s', 'wp-digital-download'), $product->post_title);
$message = sprintf(
__("Hi %s,\n\nThank you for your purchase!\n\nHere is your license key for %s:\n\n%s\n\nPlease keep this key safe. You will need it to activate and receive updates for your software.\n\nBest regards,\n%s", 'wp-digital-download'),
$order->customer_name ?: $order->customer_email,
$product->post_title,
$license_key,
get_bloginfo('name')
);
wp_mail($order->customer_email, $subject, $message);
}
/**
* Get license details for admin
*/
public static function get_license_details($license_key) {
global $wpdb;
$license = $wpdb->get_row($wpdb->prepare(
"SELECT l.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_licenses l
LEFT JOIN {$wpdb->prefix}posts p ON l.product_id = p.ID
WHERE l.license_key = %s",
$license_key
));
if (!$license) {
return null;
}
// Get activations
$license->activations = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_license_activations
WHERE license_id = %d
ORDER BY activated_at DESC",
$license->id
));
return $license;
}
}

View File

@@ -12,6 +12,15 @@ class WPDD_Metaboxes {
}
public static function add_meta_boxes() {
add_meta_box(
'wpdd_product_type',
__('Product Type', 'wp-digital-download'),
array(__CLASS__, 'render_product_type_metabox'),
'wpdd_product',
'side',
'high'
);
add_meta_box(
'wpdd_product_pricing',
__('Product Pricing', 'wp-digital-download'),
@@ -30,6 +39,15 @@ class WPDD_Metaboxes {
'high'
);
add_meta_box(
'wpdd_software_licensing',
__('Software Licensing', 'wp-digital-download'),
array(__CLASS__, 'render_software_licensing_metabox'),
'wpdd_product',
'normal',
'high'
);
add_meta_box(
'wpdd_product_settings',
__('Product Settings', 'wp-digital-download'),
@@ -78,6 +96,46 @@ class WPDD_Metaboxes {
<?php
}
public static function render_product_type_metabox($post) {
$product_type = get_post_meta($post->ID, '_wpdd_product_type', true) ?: 'digital_download';
?>
<p>
<label>
<input type="radio" name="wpdd_product_type" value="digital_download" <?php checked($product_type, 'digital_download'); ?> />
<?php _e('Digital Download', 'wp-digital-download'); ?>
</label>
<br>
<span class="description"><?php _e('Standard downloadable file (PDF, images, etc.)', 'wp-digital-download'); ?></span>
</p>
<p>
<label>
<input type="radio" name="wpdd_product_type" value="software_license" <?php checked($product_type, 'software_license'); ?> />
<?php _e('Software License', 'wp-digital-download'); ?>
</label>
<br>
<span class="description"><?php _e('WordPress plugin/theme with license key and automatic updates', 'wp-digital-download'); ?></span>
</p>
<script type="text/javascript">
jQuery(document).ready(function($) {
function toggleMetaboxes() {
var productType = $('input[name="wpdd_product_type"]:checked').val();
if (productType === 'software_license') {
$('#wpdd_software_licensing').show();
$('#wpdd_product_files').hide();
} else {
$('#wpdd_software_licensing').hide();
$('#wpdd_product_files').show();
}
}
$('input[name="wpdd_product_type"]').on('change', toggleMetaboxes);
toggleMetaboxes();
});
</script>
<?php
}
public static function render_files_metabox($post) {
$files = get_post_meta($post->ID, '_wpdd_files', true);
if (!is_array($files)) {
@@ -148,6 +206,101 @@ class WPDD_Metaboxes {
<?php
}
public static function render_software_licensing_metabox($post) {
$git_repository = get_post_meta($post->ID, '_wpdd_git_repository', true);
$git_username = get_post_meta($post->ID, '_wpdd_git_username', true);
$git_token = get_post_meta($post->ID, '_wpdd_git_token', true);
$webhook_passcode = get_post_meta($post->ID, '_wpdd_webhook_passcode', true);
$max_activations = get_post_meta($post->ID, '_wpdd_max_activations', true) ?: 1;
$license_duration = get_post_meta($post->ID, '_wpdd_license_duration', true);
$current_version = get_post_meta($post->ID, '_wpdd_current_version', true);
$min_wp_version = get_post_meta($post->ID, '_wpdd_min_wp_version', true);
$tested_wp_version = get_post_meta($post->ID, '_wpdd_tested_wp_version', true);
// Generate webhook passcode if not set
if (!$webhook_passcode) {
$webhook_passcode = wp_generate_password(32, false);
update_post_meta($post->ID, '_wpdd_webhook_passcode', $webhook_passcode);
}
$webhook_url = home_url("/wp-json/wpdd/v1/webhook/{$post->ID}/{$webhook_passcode}");
?>
<div class="wpdd-metabox-content">
<h4><?php _e('Repository Settings', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_git_repository"><?php _e('Git Repository URL', 'wp-digital-download'); ?></label><br>
<input type="url" id="wpdd_git_repository" name="wpdd_git_repository" value="<?php echo esc_attr($git_repository); ?>" class="widefat" placeholder="https://github.com/username/repository" />
<span class="description"><?php _e('Full URL to your Git repository', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_git_username"><?php _e('Git Username (optional)', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_git_username" name="wpdd_git_username" value="<?php echo esc_attr($git_username); ?>" class="widefat" />
<span class="description"><?php _e('For private repositories', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_git_token"><?php _e('Git Access Token (optional)', 'wp-digital-download'); ?></label><br>
<input type="password" id="wpdd_git_token" name="wpdd_git_token" value="<?php echo esc_attr($git_token); ?>" class="widefat" />
<span class="description"><?php _e('Personal access token for private repositories', 'wp-digital-download'); ?></span>
</p>
<h4><?php _e('Webhook Configuration', 'wp-digital-download'); ?></h4>
<p>
<label><?php _e('Webhook URL', 'wp-digital-download'); ?></label><br>
<input type="text" value="<?php echo esc_attr($webhook_url); ?>" class="widefat" readonly onclick="this.select();" />
<span class="description"><?php _e('Add this URL to your repository webhook settings (GitHub, GitLab, etc.) to receive release notifications', 'wp-digital-download'); ?></span>
</p>
<p>
<button type="button" class="button" onclick="wpddRegenerateWebhookPasscode(<?php echo $post->ID; ?>)">
<?php _e('Regenerate Webhook URL', 'wp-digital-download'); ?>
</button>
</p>
<h4><?php _e('License Settings', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_max_activations"><?php _e('Max Activations per License', 'wp-digital-download'); ?></label><br>
<input type="number" id="wpdd_max_activations" name="wpdd_max_activations" value="<?php echo esc_attr($max_activations); ?>" min="1" />
<span class="description"><?php _e('Number of sites where the license can be activated', 'wp-digital-download'); ?></span>
</p>
<p>
<label for="wpdd_license_duration"><?php _e('License Duration (days)', 'wp-digital-download'); ?></label><br>
<input type="number" id="wpdd_license_duration" name="wpdd_license_duration" value="<?php echo esc_attr($license_duration); ?>" min="0" />
<span class="description"><?php _e('Leave blank or 0 for lifetime licenses', 'wp-digital-download'); ?></span>
</p>
<h4><?php _e('Version Information', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_current_version"><?php _e('Current Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_current_version" name="wpdd_current_version" value="<?php echo esc_attr($current_version); ?>" placeholder="1.0.0" />
</p>
<p>
<label for="wpdd_min_wp_version"><?php _e('Minimum WordPress Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_min_wp_version" name="wpdd_min_wp_version" value="<?php echo esc_attr($min_wp_version); ?>" placeholder="5.0" />
</p>
<p>
<label for="wpdd_tested_wp_version"><?php _e('Tested up to WordPress Version', 'wp-digital-download'); ?></label><br>
<input type="text" id="wpdd_tested_wp_version" name="wpdd_tested_wp_version" value="<?php echo esc_attr($tested_wp_version); ?>" placeholder="6.4" />
</p>
</div>
<script type="text/javascript">
function wpddRegenerateWebhookPasscode(productId) {
if (confirm('<?php _e('Are you sure? This will invalidate the existing webhook URL.', 'wp-digital-download'); ?>')) {
var newPasscode = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
jQuery('#wpdd_webhook_passcode').val(newPasscode);
var newUrl = '<?php echo home_url("/wp-json/wpdd/v1/webhook/"); ?>' + productId + '/' + newPasscode;
jQuery('input[readonly]').val(newUrl);
}
}
</script>
<?php
}
public static function render_settings_metabox($post) {
$download_limit = get_post_meta($post->ID, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($post->ID, '_wpdd_download_expiry', true);
@@ -304,5 +457,52 @@ class WPDD_Metaboxes {
update_post_meta($post_id, '_wpdd_watermark_text',
sanitize_text_field($_POST['wpdd_watermark_text']));
}
// Product type
if (isset($_POST['wpdd_product_type'])) {
update_post_meta($post_id, '_wpdd_product_type',
sanitize_text_field($_POST['wpdd_product_type']));
}
// Software licensing fields
if (isset($_POST['wpdd_git_repository'])) {
update_post_meta($post_id, '_wpdd_git_repository',
esc_url_raw($_POST['wpdd_git_repository']));
}
if (isset($_POST['wpdd_git_username'])) {
update_post_meta($post_id, '_wpdd_git_username',
sanitize_text_field($_POST['wpdd_git_username']));
}
if (isset($_POST['wpdd_git_token'])) {
update_post_meta($post_id, '_wpdd_git_token',
sanitize_text_field($_POST['wpdd_git_token']));
}
if (isset($_POST['wpdd_max_activations'])) {
update_post_meta($post_id, '_wpdd_max_activations',
intval($_POST['wpdd_max_activations']));
}
if (isset($_POST['wpdd_license_duration'])) {
update_post_meta($post_id, '_wpdd_license_duration',
intval($_POST['wpdd_license_duration']));
}
if (isset($_POST['wpdd_current_version'])) {
update_post_meta($post_id, '_wpdd_current_version',
sanitize_text_field($_POST['wpdd_current_version']));
}
if (isset($_POST['wpdd_min_wp_version'])) {
update_post_meta($post_id, '_wpdd_min_wp_version',
sanitize_text_field($_POST['wpdd_min_wp_version']));
}
if (isset($_POST['wpdd_tested_wp_version'])) {
update_post_meta($post_id, '_wpdd_tested_wp_version',
sanitize_text_field($_POST['wpdd_tested_wp_version']));
}
}
}

View File

@@ -15,7 +15,26 @@ class WPDD_PayPal {
}
public static function enqueue_paypal_sdk() {
if (!is_page(get_option('wpdd_checkout_page_id'))) {
// Check if we're on a page with the checkout shortcode or if there's a product_id in the URL (checkout page)
global $post;
$is_checkout = false;
// Check if we're on the configured checkout page
if (is_page(get_option('wpdd_checkout_page_id'))) {
$is_checkout = true;
}
// Also check if the current page has the checkout shortcode
if ($post && has_shortcode($post->post_content, 'wpdd_checkout')) {
$is_checkout = true;
}
// Also check if there's a product_id parameter (checkout flow)
if (isset($_GET['product_id']) || isset($_GET['wpdd_action'])) {
$is_checkout = true;
}
if (!$is_checkout) {
return;
}
@@ -183,6 +202,9 @@ class WPDD_PayPal {
$order_id = $wpdb->insert_id;
// Trigger the order completed hook for balance tracking
do_action('wpdd_order_completed', $order_id);
self::generate_download_link($order_id);
self::send_purchase_email($order_id);

View File

@@ -0,0 +1,169 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Setup {
public static function init() {
add_action('admin_notices', array(__CLASS__, 'show_setup_notice'));
add_action('wp_ajax_wpdd_dismiss_setup', array(__CLASS__, 'dismiss_setup_notice'));
add_action('wp_ajax_wpdd_create_pages', array(__CLASS__, 'create_default_pages'));
}
public static function show_setup_notice() {
if (!get_option('wpdd_show_setup_notice') || get_option('wpdd_setup_completed')) {
return;
}
if (!current_user_can('manage_options')) {
return;
}
$screen = get_current_screen();
if ($screen && strpos($screen->id, 'wpdd_product') === false && $screen->id !== 'dashboard') {
return;
}
?>
<div class="notice notice-info wpdd-setup-notice" style="position: relative;">
<h3><?php _e('🎉 WP Digital Download Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('Thank you for installing WP Digital Download! To get started, you can create the default pages or set them up manually.', 'wp-digital-download'); ?></p>
<div class="wpdd-setup-actions" style="margin-bottom: 10px;">
<button type="button" class="button button-primary" id="wpdd-create-pages">
<?php _e('Create Default Pages', 'wp-digital-download'); ?>
</button>
<button type="button" class="button button-secondary" id="wpdd-skip-setup">
<?php _e('Skip - I\'ll Set Up Manually', 'wp-digital-download'); ?>
</button>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings'); ?>" class="button">
<?php _e('Go to Settings', 'wp-digital-download'); ?>
</a>
</div>
<div class="wpdd-pages-preview" style="background: #f9f9f9; padding: 15px; margin: 10px 0; border-radius: 4px;">
<h4><?php _e('Default Pages to be Created:', 'wp-digital-download'); ?></h4>
<ul style="columns: 2; margin: 0;">
<li><strong><?php _e('Shop', 'wp-digital-download'); ?></strong> - <?php _e('Product catalog with filtering', 'wp-digital-download'); ?></li>
<li><strong><?php _e('My Purchases', 'wp-digital-download'); ?></strong> - <?php _e('Customer purchase history', 'wp-digital-download'); ?></li>
<li><strong><?php _e('Checkout', 'wp-digital-download'); ?></strong> - <?php _e('Payment and order form', 'wp-digital-download'); ?></li>
<li><strong><?php _e('Thank You', 'wp-digital-download'); ?></strong> - <?php _e('Order confirmation page', 'wp-digital-download'); ?></li>
</ul>
</div>
<div class="wpdd-setup-status" id="wpdd-setup-status" style="display: none; padding: 10px; margin: 10px 0; border-radius: 4px;">
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#wpdd-create-pages').on('click', function() {
var $button = $(this);
var $status = $('#wpdd-setup-status');
$button.prop('disabled', true).text('<?php _e('Creating Pages...', 'wp-digital-download'); ?>');
$status.show().removeClass().addClass('notice notice-info').html('<p><?php _e('Creating default pages...', 'wp-digital-download'); ?></p>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_create_pages',
nonce: '<?php echo wp_create_nonce('wpdd_setup_nonce'); ?>'
},
success: function(response) {
if (response.success) {
$status.removeClass().addClass('notice notice-success').html(
'<p><strong><?php _e('Success!', 'wp-digital-download'); ?></strong> ' + response.data.message + '</p>' +
'<ul>' + response.data.pages.map(function(page) {
return '<li><a href="' + page.edit_url + '">' + page.title + '</a> - <a href="' + page.view_url + '" target="_blank"><?php _e('View', 'wp-digital-download'); ?></a></li>';
}).join('') + '</ul>'
);
$('.wpdd-setup-actions').hide();
setTimeout(function() {
$('.wpdd-setup-notice').fadeOut();
}, 5000);
} else {
$status.removeClass().addClass('notice notice-error').html('<p><strong><?php _e('Error:', 'wp-digital-download'); ?></strong> ' + (response.data || '<?php _e('Unknown error occurred', 'wp-digital-download'); ?>') + '</p>');
$button.prop('disabled', false).text('<?php _e('Create Default Pages', 'wp-digital-download'); ?>');
}
},
error: function() {
$status.removeClass().addClass('notice notice-error').html('<p><strong><?php _e('Error:', 'wp-digital-download'); ?></strong> <?php _e('Failed to create pages. Please try again.', 'wp-digital-download'); ?></p>');
$button.prop('disabled', false).text('<?php _e('Create Default Pages', 'wp-digital-download'); ?>');
}
});
});
$('#wpdd-skip-setup').on('click', function() {
var $button = $(this);
$button.prop('disabled', true).text('<?php _e('Dismissing...', 'wp-digital-download'); ?>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_dismiss_setup',
nonce: '<?php echo wp_create_nonce('wpdd_setup_nonce'); ?>'
},
success: function(response) {
$('.wpdd-setup-notice').fadeOut();
}
});
});
});
</script>
<?php
}
public static function create_default_pages() {
check_ajax_referer('wpdd_setup_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied.', 'wp-digital-download'));
}
$created_pages = WPDD_Install::create_pages_optional();
if (!empty($created_pages)) {
$pages_info = array();
foreach ($created_pages as $page_id) {
$page = get_post($page_id);
if ($page) {
$pages_info[] = array(
'title' => $page->post_title,
'edit_url' => admin_url('post.php?post=' . $page_id . '&action=edit'),
'view_url' => get_permalink($page_id)
);
}
}
update_option('wpdd_setup_completed', true);
delete_option('wpdd_show_setup_notice');
wp_send_json_success(array(
'message' => sprintf(__('%d pages created successfully!', 'wp-digital-download'), count($pages_info)),
'pages' => $pages_info
));
} else {
wp_send_json_error(__('No pages were created. They may already exist.', 'wp-digital-download'));
}
}
public static function dismiss_setup_notice() {
check_ajax_referer('wpdd_setup_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied.', 'wp-digital-download'));
}
update_option('wpdd_setup_completed', true);
delete_option('wpdd_show_setup_notice');
wp_send_json_success();
}
}

View File

@@ -0,0 +1,471 @@
<?php
/**
* WPDD Plugin Updater Library
*
* Include this file in your WordPress plugin to enable automatic updates
* and license validation through the WP Digital Download licensing system.
*
* @version 1.0.0
* @author WP Digital Download
*/
if (!defined('ABSPATH')) {
exit;
}
if (!class_exists('WPDD_Plugin_Updater')) {
class WPDD_Plugin_Updater {
private $plugin_slug;
private $plugin_file;
private $version;
private $license_key;
private $update_server;
private $transient_key;
/**
* Initialize the updater
*
* @param string $plugin_file Full path to the main plugin file
* @param string $license_key Your license key
* @param string $update_server URL to your update server
* @param array $args Additional arguments
*/
public function __construct($plugin_file, $license_key, $update_server, $args = array()) {
$this->plugin_file = $plugin_file;
$this->plugin_slug = basename($plugin_file, '.php');
$this->license_key = $license_key;
$this->update_server = trailingslashit($update_server);
// Get plugin version from header
if (!function_exists('get_plugin_data')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_data = get_plugin_data($plugin_file);
$this->version = $plugin_data['Version'];
$this->transient_key = 'wpdd_update_' . $this->plugin_slug;
// Initialize hooks
$this->init_hooks();
// Add settings page if requested
if (isset($args['add_settings_page']) && $args['add_settings_page']) {
$this->add_settings_page();
}
}
/**
* Initialize WordPress hooks
*/
private function init_hooks() {
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_update'));
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
add_filter('upgrader_pre_download', array($this, 'maybe_download_package'), 10, 3);
// Clean up transients on plugin activation/deactivation
register_activation_hook($this->plugin_file, array($this, 'delete_transients'));
register_deactivation_hook($this->plugin_file, array($this, 'delete_transients'));
}
/**
* Check for plugin updates
*/
public function check_for_update($transient) {
if (empty($transient->checked)) {
return $transient;
}
// Get cached update info
$update_cache = get_transient($this->transient_key);
if ($update_cache !== false) {
if (isset($update_cache->update_available) && $update_cache->update_available) {
$transient->response[$this->plugin_file] = $update_cache;
}
return $transient;
}
// Check for update from server
$update_info = $this->request_update_info();
if ($update_info && isset($update_info['update_available']) && $update_info['update_available']) {
$plugin_data = array(
'slug' => $this->plugin_slug,
'plugin' => $this->plugin_file,
'new_version' => $update_info['version'],
'url' => $update_info['url'],
'package' => $update_info['package'],
'tested' => $update_info['tested'],
'requires' => $update_info['requires'],
'requires_php' => $update_info['requires_php'],
'compatibility' => new stdClass()
);
$update_cache = (object) $plugin_data;
$update_cache->update_available = true;
// Cache for 12 hours
set_transient($this->transient_key, $update_cache, 12 * HOUR_IN_SECONDS);
$transient->response[$this->plugin_file] = $update_cache;
} else {
// No update available - cache negative result for 12 hours
$update_cache = new stdClass();
$update_cache->update_available = false;
set_transient($this->transient_key, $update_cache, 12 * HOUR_IN_SECONDS);
}
return $transient;
}
/**
* Provide plugin information for the update screen
*/
public function plugin_info($false, $action, $args) {
if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
return $false;
}
$update_info = $this->request_update_info();
if (!$update_info) {
return $false;
}
return (object) array(
'slug' => $this->plugin_slug,
'name' => $update_info['name'] ?? $this->plugin_slug,
'version' => $update_info['version'] ?? $this->version,
'author' => $update_info['author'] ?? '',
'homepage' => $update_info['url'] ?? '',
'requires' => $update_info['requires'] ?? '5.0',
'tested' => $update_info['tested'] ?? get_bloginfo('version'),
'requires_php' => $update_info['requires_php'] ?? '7.0',
'download_link' => $update_info['package'] ?? '',
'sections' => array(
'changelog' => $update_info['changelog'] ?? '',
'description' => $update_info['description'] ?? ''
),
'banners' => array(),
'icons' => array()
);
}
/**
* Handle package download with license validation
*/
public function maybe_download_package($reply, $package, $upgrader) {
// Check if this is our plugin's package
if (strpos($package, $this->update_server) === false || strpos($package, $this->plugin_slug) === false) {
return $reply;
}
// Validate license before download
$license_valid = $this->validate_license();
if (!$license_valid) {
return new WP_Error('license_invalid', __('Your license key is invalid or expired. Please update your license key.'));
}
return $reply;
}
/**
* Request update information from server
*/
private function request_update_info() {
$url = $this->update_server . "wp-json/wpdd/v1/check-update/{$this->plugin_slug}";
$url = add_query_arg(array(
'license_key' => $this->license_key,
'version' => $this->version,
'site_url' => home_url()
), $url);
$response = wp_remote_get($url, array(
'timeout' => 15,
'headers' => array(
'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url()
)
));
if (is_wp_error($response)) {
error_log('WPDD Updater: Failed to check for updates - ' . $response->get_error_message());
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (!$data || !isset($data['success'])) {
error_log('WPDD Updater: Invalid response from update server');
return false;
}
if (!$data['success']) {
if (isset($data['error'])) {
error_log('WPDD Updater: ' . $data['error'] . ' - ' . ($data['message'] ?? ''));
}
return false;
}
return $data;
}
/**
* Validate license with server
*/
public function validate_license() {
if (empty($this->license_key)) {
return false;
}
$url = $this->update_server . 'wp-json/wpdd/v1/validate-license';
$response = wp_remote_post($url, array(
'timeout' => 15,
'body' => array(
'license_key' => $this->license_key,
'product_slug' => $this->plugin_slug,
'site_url' => home_url()
),
'headers' => array(
'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url()
)
));
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data && isset($data['success']) && $data['success'];
}
/**
* Activate license
*/
public function activate_license($license_key = null) {
if ($license_key) {
$this->license_key = $license_key;
}
if (empty($this->license_key)) {
return array(
'success' => false,
'message' => __('Please enter a license key.')
);
}
$url = $this->update_server . 'wp-json/wpdd/v1/activate-license';
$response = wp_remote_post($url, array(
'timeout' => 15,
'body' => array(
'license_key' => $this->license_key,
'site_url' => home_url(),
'site_name' => get_bloginfo('name'),
'wp_version' => get_bloginfo('version'),
'php_version' => PHP_VERSION
),
'headers' => array(
'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url()
)
));
if (is_wp_error($response)) {
return array(
'success' => false,
'message' => $response->get_error_message()
);
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (!$data) {
return array(
'success' => false,
'message' => __('Invalid response from server.')
);
}
return $data;
}
/**
* Deactivate license
*/
public function deactivate_license() {
if (empty($this->license_key)) {
return array(
'success' => false,
'message' => __('No license key to deactivate.')
);
}
$url = $this->update_server . 'wp-json/wpdd/v1/deactivate-license';
$response = wp_remote_post($url, array(
'timeout' => 15,
'body' => array(
'license_key' => $this->license_key,
'site_url' => home_url()
),
'headers' => array(
'User-Agent' => 'WPDD-Updater/' . $this->version . '; ' . home_url()
)
));
if (is_wp_error($response)) {
return array(
'success' => false,
'message' => $response->get_error_message()
);
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data ?: array(
'success' => false,
'message' => __('Invalid response from server.')
);
}
/**
* Add a simple settings page for license management
*/
private function add_settings_page() {
add_action('admin_menu', array($this, 'add_license_menu'));
add_action('admin_init', array($this, 'handle_license_actions'));
}
/**
* Add license menu page
*/
public function add_license_menu() {
add_options_page(
sprintf(__('%s License', 'default'), $this->plugin_slug),
sprintf(__('%s License', 'default'), $this->plugin_slug),
'manage_options',
$this->plugin_slug . '-license',
array($this, 'render_license_page')
);
}
/**
* Handle license activation/deactivation
*/
public function handle_license_actions() {
if (!current_user_can('manage_options')) {
return;
}
$option_key = $this->plugin_slug . '_license_key';
if (isset($_POST['activate_license'])) {
if (!wp_verify_nonce($_POST['license_nonce'], 'wpdd_license_nonce')) {
return;
}
$license_key = sanitize_text_field($_POST['license_key']);
$result = $this->activate_license($license_key);
if ($result['success']) {
update_option($option_key, $license_key);
$this->license_key = $license_key;
add_settings_error('wpdd_license', 'activated', $result['message'], 'updated');
} else {
add_settings_error('wpdd_license', 'activation_failed', $result['message'], 'error');
}
}
if (isset($_POST['deactivate_license'])) {
if (!wp_verify_nonce($_POST['license_nonce'], 'wpdd_license_nonce')) {
return;
}
$result = $this->deactivate_license();
if ($result['success']) {
delete_option($option_key);
$this->license_key = '';
add_settings_error('wpdd_license', 'deactivated', $result['message'], 'updated');
} else {
add_settings_error('wpdd_license', 'deactivation_failed', $result['message'], 'error');
}
}
}
/**
* Render license management page
*/
public function render_license_page() {
$option_key = $this->plugin_slug . '_license_key';
$license_key = get_option($option_key, '');
$license_status = $this->validate_license();
?>
<div class="wrap">
<h1><?php printf(__('%s License Settings', 'default'), esc_html($this->plugin_slug)); ?></h1>
<?php settings_errors('wpdd_license'); ?>
<form method="post" action="">
<?php wp_nonce_field('wpdd_license_nonce', 'license_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="license_key"><?php _e('License Key', 'default'); ?></label>
</th>
<td>
<input type="text" id="license_key" name="license_key"
value="<?php echo esc_attr($license_key); ?>" class="regular-text" />
<?php if ($license_status): ?>
<span class="dashicons dashicons-yes-alt" style="color: green;"></span>
<span style="color: green;"><?php _e('Active', 'default'); ?></span>
<?php elseif (!empty($license_key)): ?>
<span class="dashicons dashicons-dismiss" style="color: red;"></span>
<span style="color: red;"><?php _e('Invalid/Expired', 'default'); ?></span>
<?php endif; ?>
</td>
</tr>
</table>
<?php if (empty($license_key) || !$license_status): ?>
<p class="submit">
<input type="submit" name="activate_license" class="button-primary"
value="<?php _e('Activate License', 'default'); ?>" />
</p>
<?php else: ?>
<p class="submit">
<input type="submit" name="deactivate_license" class="button-secondary"
value="<?php _e('Deactivate License', 'default'); ?>" />
</p>
<?php endif; ?>
</form>
<h2><?php _e('Instructions', 'default'); ?></h2>
<ol>
<li><?php _e('Enter your license key above and click "Activate License"', 'default'); ?></li>
<li><?php _e('Once activated, you will receive automatic updates for this plugin', 'default'); ?></li>
<li><?php _e('You can deactivate the license if you want to use it on a different site', 'default'); ?></li>
</ol>
</div>
<?php
}
/**
* Delete update transients
*/
public function delete_transients() {
delete_transient($this->transient_key);
delete_site_transient('update_plugins');
}
}
} // End class exists check