Adding more functionality

This commit is contained in:
2025-08-29 18:54:14 -07:00
parent 264e65006a
commit ce48f1615f
14 changed files with 4098 additions and 84 deletions

View File

@@ -10,6 +10,10 @@ class WPDD_Admin_Payouts {
add_action('admin_menu', array(__CLASS__, 'add_menu_page'));
add_action('admin_post_wpdd_process_payout', array(__CLASS__, 'process_payout'));
add_action('admin_post_wpdd_bulk_payouts', array(__CLASS__, 'process_bulk_payouts'));
add_action('admin_post_wpdd_process_payout_request', array(__CLASS__, 'process_payout_request'));
add_action('admin_post_wpdd_reject_payout_request', array(__CLASS__, 'reject_payout_request'));
add_action('admin_post_wpdd_manual_payout', array(__CLASS__, 'process_manual_payout'));
add_action('admin_post_wpdd_adjust_balance', array(__CLASS__, 'adjust_creator_balance'));
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_scripts'));
}
@@ -50,6 +54,15 @@ class WPDD_Admin_Payouts {
$currency = get_option('wpdd_currency', 'USD');
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
// Get payout requests (requested status)
$payout_requests = $wpdb->get_results(
"SELECT p.*, u.display_name, u.user_email
FROM {$wpdb->prefix}wpdd_payouts p
INNER JOIN {$wpdb->users} u ON p.creator_id = u.ID
WHERE p.status = 'requested'
ORDER BY p.created_at ASC"
);
// Get payout history
$query = "SELECT p.*, u.display_name, u.user_email
FROM {$wpdb->prefix}wpdd_payouts p
@@ -81,9 +94,172 @@ class WPDD_Admin_Payouts {
<div class="notice notice-error is-dismissible">
<p><?php _e('Error processing payout. Please try again.', 'wp-digital-download'); ?></p>
</div>
<?php elseif ($_GET['message'] === 'balance_adjusted') : ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Creator balance adjusted successfully.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($payout_requests)) : ?>
<div class="wpdd-payout-requests" style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 20px; margin: 20px 0;">
<h2 style="margin-top: 0;"><?php _e('Payout Requests', 'wp-digital-download'); ?> <span class="count">(<?php echo count($payout_requests); ?>)</span></h2>
<p><?php _e('Creators have requested the following payouts:', 'wp-digital-download'); ?></p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Request Date', 'wp-digital-download'); ?></th>
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($payout_requests as $request) : ?>
<tr>
<td>
<strong><?php echo esc_html($request->display_name); ?></strong><br>
<small><?php echo esc_html($request->user_email); ?></small>
</td>
<td><strong><?php echo wpdd_format_price($request->amount, $request->currency); ?></strong></td>
<td><?php echo esc_html($request->paypal_email); ?></td>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($request->created_at))); ?></td>
<td>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
<input type="hidden" name="action" value="wpdd_process_payout_request">
<input type="hidden" name="payout_id" value="<?php echo esc_attr($request->id); ?>">
<?php wp_nonce_field('wpdd_process_payout_request_' . $request->id, 'wpdd_nonce'); ?>
<button type="submit" class="button button-primary">
<?php _e('Process Now', 'wp-digital-download'); ?>
</button>
</form>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline; margin-left: 5px;">
<input type="hidden" name="action" value="wpdd_reject_payout_request">
<input type="hidden" name="payout_id" value="<?php echo esc_attr($request->id); ?>">
<?php wp_nonce_field('wpdd_reject_payout_request_' . $request->id, 'wpdd_nonce'); ?>
<button type="submit" class="button button-secondary" onclick="return confirm('<?php _e('Are you sure you want to reject this payout request?', 'wp-digital-download'); ?>')">
<?php _e('Reject', 'wp-digital-download'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Manual Actions Section -->
<div class="wpdd-manual-actions" style="display: flex; gap: 20px; margin: 20px 0;">
<!-- Manual Payout Form -->
<div style="background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; flex: 1;">
<h3 style="margin-top: 0;"><?php _e('Manual Payout', 'wp-digital-download'); ?></h3>
<p><?php _e('Process a payout for off-site sales or manual adjustments.', 'wp-digital-download'); ?></p>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wpdd_manual_payout">
<?php wp_nonce_field('wpdd_manual_payout', 'wpdd_nonce'); ?>
<table class="form-table" style="margin: 0;">
<tr>
<th><label for="creator_id"><?php _e('Creator', 'wp-digital-download'); ?></label></th>
<td>
<select name="creator_id" id="creator_id" required style="width: 100%;">
<option value=""><?php _e('Select Creator', 'wp-digital-download'); ?></option>
<?php
$all_creators = get_users(array('role' => 'wpdd_creator'));
foreach ($all_creators as $creator) :
$paypal_email = get_user_meta($creator->ID, 'wpdd_paypal_email', true);
?>
<option value="<?php echo esc_attr($creator->ID); ?>" <?php echo empty($paypal_email) ? 'disabled' : ''; ?>>
<?php echo esc_html($creator->display_name); ?>
<?php echo empty($paypal_email) ? ' (' . __('No PayPal email', 'wp-digital-download') . ')' : ' (' . esc_html($paypal_email) . ')'; ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th><label for="payout_amount"><?php _e('Amount', 'wp-digital-download'); ?></label></th>
<td>
<input type="number" name="payout_amount" id="payout_amount" step="0.01" min="0.01" required style="width: 150px;">
<span><?php echo esc_html($currency); ?></span>
</td>
</tr>
<tr>
<th><label for="payout_reason"><?php _e('Reason/Note', 'wp-digital-download'); ?></label></th>
<td>
<textarea name="payout_reason" id="payout_reason" rows="3" style="width: 100%;" placeholder="<?php _e('e.g., Off-site sale, manual adjustment, etc.', 'wp-digital-download'); ?>"></textarea>
</td>
</tr>
</table>
<p class="submit" style="margin: 15px 0 0 0;">
<button type="submit" class="button button-primary">
<?php _e('Process Manual Payout', 'wp-digital-download'); ?>
</button>
</p>
</form>
</div>
<!-- Balance Adjustment Form -->
<div style="background: white; border: 1px solid #ddd; border-radius: 4px; padding: 20px; flex: 1;">
<h3 style="margin-top: 0;"><?php _e('Adjust Creator Balance', 'wp-digital-download'); ?></h3>
<p><?php _e('Add or subtract funds from a creator\'s balance.', 'wp-digital-download'); ?></p>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wpdd_adjust_balance">
<?php wp_nonce_field('wpdd_adjust_balance', 'wpdd_nonce'); ?>
<table class="form-table" style="margin: 0;">
<tr>
<th><label for="adj_creator_id"><?php _e('Creator', 'wp-digital-download'); ?></label></th>
<td>
<select name="creator_id" id="adj_creator_id" required style="width: 100%;">
<option value=""><?php _e('Select Creator', 'wp-digital-download'); ?></option>
<?php foreach ($all_creators as $creator) : ?>
<option value="<?php echo esc_attr($creator->ID); ?>">
<?php echo esc_html($creator->display_name); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th><label for="adjustment_type"><?php _e('Type', 'wp-digital-download'); ?></label></th>
<td>
<select name="adjustment_type" id="adjustment_type" required>
<option value="add"><?php _e('Add Funds', 'wp-digital-download'); ?></option>
<option value="subtract"><?php _e('Subtract Funds', 'wp-digital-download'); ?></option>
</select>
</td>
</tr>
<tr>
<th><label for="adjustment_amount"><?php _e('Amount', 'wp-digital-download'); ?></label></th>
<td>
<input type="number" name="adjustment_amount" id="adjustment_amount" step="0.01" min="0.01" required style="width: 150px;">
<span><?php echo esc_html($currency); ?></span>
</td>
</tr>
<tr>
<th><label for="adjustment_reason"><?php _e('Reason/Note', 'wp-digital-download'); ?></label></th>
<td>
<textarea name="adjustment_reason" id="adjustment_reason" rows="3" style="width: 100%;" placeholder="<?php _e('e.g., Manual adjustment, refund, bonus, etc.', 'wp-digital-download'); ?>" required></textarea>
</td>
</tr>
</table>
<p class="submit" style="margin: 15px 0 0 0;">
<button type="submit" class="button button-secondary">
<?php _e('Adjust Balance', 'wp-digital-download'); ?>
</button>
</p>
</form>
</div>
</div>
<div class="wpdd-payout-stats">
<h2><?php _e('Pending Payouts', 'wp-digital-download'); ?></h2>
@@ -432,4 +608,241 @@ class WPDD_Admin_Payouts {
return false;
}
}
public static function process_payout_request() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to perform this action.', 'wp-digital-download'));
}
$payout_id = isset($_POST['payout_id']) ? intval($_POST['payout_id']) : 0;
if (!$payout_id || !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_process_payout_request_' . $payout_id)) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
global $wpdb;
// Get the payout request
$payout = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d AND status = 'requested'",
$payout_id
));
if (!$payout) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
// Process via PayPal API
$result = WPDD_PayPal_Payouts::process_payout($payout_id);
if ($result['success']) {
// Update payout status
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array(
'status' => 'completed',
'transaction_id' => $result['transaction_id'],
'processed_by' => get_current_user_id(),
'processed_at' => current_time('mysql')
),
array('id' => $payout_id),
array('%s', '%s', '%d', '%s'),
array('%d')
);
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success'));
} else {
// Update with error
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array(
'status' => 'failed',
'notes' => $result['error']
),
array('id' => $payout_id),
array('%s', '%s'),
array('%d')
);
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
}
exit;
}
public static function reject_payout_request() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to perform this action.', 'wp-digital-download'));
}
$payout_id = isset($_POST['payout_id']) ? intval($_POST['payout_id']) : 0;
if (!$payout_id || !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_reject_payout_request_' . $payout_id)) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
global $wpdb;
// Get the payout request
$payout = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d AND status = 'requested'",
$payout_id
));
if (!$payout) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
// Update status to failed/rejected
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array(
'status' => 'failed',
'notes' => 'Request rejected by administrator',
'processed_by' => get_current_user_id(),
'processed_at' => current_time('mysql')
),
array('id' => $payout_id),
array('%s', '%s', '%d', '%s'),
array('%d')
);
// Restore balance to creator
update_user_meta($payout->creator_id, 'wpdd_creator_balance', $payout->amount);
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success'));
exit;
}
public static function process_manual_payout() {
if (!current_user_can('manage_options') ||
!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_manual_payout')) {
wp_die(__('Security check failed', 'wp-digital-download'));
}
$creator_id = intval($_POST['creator_id']);
$amount = floatval($_POST['payout_amount']);
$reason = sanitize_textarea_field($_POST['payout_reason']);
if (!$creator_id || $amount <= 0) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
$creator = get_userdata($creator_id);
if (!$creator || !in_array('wpdd_creator', $creator->roles)) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
$paypal_email = get_user_meta($creator_id, 'wpdd_paypal_email', true);
if (empty($paypal_email)) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
global $wpdb;
// Create the payout record
$result = $wpdb->insert(
$wpdb->prefix . 'wpdd_payouts',
array(
'creator_id' => $creator_id,
'amount' => $amount,
'currency' => get_option('wpdd_currency', 'USD'),
'paypal_email' => $paypal_email,
'status' => 'pending',
'payout_method' => 'manual',
'notes' => $reason,
'created_at' => current_time('mysql'),
'processed_by' => get_current_user_id()
),
array('%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
);
if (!$result) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
$payout_id = $wpdb->insert_id;
// Try to process via PayPal
if (class_exists('WPDD_PayPal_Payouts')) {
$paypal_result = WPDD_PayPal_Payouts::process_payout($payout_id);
if (!$paypal_result) {
// Update status to failed
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array('status' => 'failed'),
array('id' => $payout_id),
array('%s'),
array('%d')
);
}
}
wp_redirect(add_query_arg('message', 'success', wp_get_referer()));
exit;
}
public static function adjust_creator_balance() {
if (!current_user_can('manage_options') ||
!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_adjust_balance')) {
wp_die(__('Security check failed', 'wp-digital-download'));
}
$creator_id = intval($_POST['creator_id']);
$adjustment_type = sanitize_text_field($_POST['adjustment_type']);
$amount = floatval($_POST['adjustment_amount']);
$reason = sanitize_textarea_field($_POST['adjustment_reason']);
if (!$creator_id || $amount <= 0 || !in_array($adjustment_type, array('add', 'subtract'))) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
$creator = get_userdata($creator_id);
if (!$creator || !in_array('wpdd_creator', $creator->roles)) {
wp_redirect(add_query_arg('message', 'error', wp_get_referer()));
exit;
}
// Get current balance
$current_balance = floatval(get_user_meta($creator_id, 'wpdd_balance', true));
// Calculate new balance
if ($adjustment_type === 'add') {
$new_balance = $current_balance + $amount;
} else {
$new_balance = max(0, $current_balance - $amount); // Don't allow negative balance
}
// Update the balance
update_user_meta($creator_id, 'wpdd_balance', $new_balance);
// Create a record of this adjustment
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'wpdd_balance_adjustments',
array(
'creator_id' => $creator_id,
'adjustment_type' => $adjustment_type,
'amount' => $amount,
'previous_balance' => $current_balance,
'new_balance' => $new_balance,
'reason' => $reason,
'adjusted_by' => get_current_user_id(),
'created_at' => current_time('mysql')
),
array('%d', '%s', '%f', '%f', '%f', '%s', '%d', '%s')
);
wp_redirect(add_query_arg('message', 'balance_adjusted', wp_get_referer()));
exit;
}
}

View File

@@ -12,6 +12,7 @@ class WPDD_Admin {
add_action('manage_wpdd_product_posts_custom_column', array(__CLASS__, 'render_product_columns'), 10, 2);
add_filter('manage_edit-wpdd_product_sortable_columns', array(__CLASS__, 'make_columns_sortable'));
add_action('pre_get_posts', array(__CLASS__, 'sort_products_by_column'));
add_action('pre_get_posts', array(__CLASS__, 'filter_creator_products'));
add_action('admin_init', array(__CLASS__, 'handle_admin_actions'));
// Initialize admin payouts
@@ -21,6 +22,13 @@ class WPDD_Admin {
}
public static function add_admin_menus() {
// Show different menus based on user role
$user = wp_get_current_user();
$is_creator = in_array('wpdd_creator', (array) $user->roles);
$is_admin = current_user_can('manage_options');
if ($is_admin) {
// Full admin menus
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Orders', 'wp-digital-download'),
@@ -58,6 +66,28 @@ class WPDD_Admin {
);
}
if ($is_creator || $is_admin) {
// Creator-specific menus
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('My Sales', 'wp-digital-download'),
__('My Sales', 'wp-digital-download'),
'wpdd_view_own_sales',
'wpdd-creator-sales',
array(__CLASS__, 'render_creator_sales_page')
);
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('My Payouts', 'wp-digital-download'),
__('My Payouts', 'wp-digital-download'),
'wpdd_view_own_sales',
'wpdd-creator-payouts',
array(__CLASS__, 'render_creator_payouts_page')
);
}
}
public static function add_product_columns($columns) {
$new_columns = array();
@@ -917,4 +947,289 @@ class WPDD_Admin {
<?php
}
public static function filter_creator_products($query) {
if (!is_admin() || !$query->is_main_query()) {
return;
}
if (!isset($_GET['post_type']) || $_GET['post_type'] !== 'wpdd_product') {
return;
}
$user = wp_get_current_user();
$is_creator = in_array('wpdd_creator', (array) $user->roles);
$is_admin = current_user_can('manage_options');
// Only filter for creators, not admins
if ($is_creator && !$is_admin) {
$query->set('author', get_current_user_id());
}
}
public static function render_creator_sales_page() {
global $wpdb;
$user_id = get_current_user_id();
$currency = get_option('wpdd_currency', 'USD');
$commission_rate = floatval(get_option('wpdd_commission_rate', 0));
// Get creator's sales data
$sales = $wpdb->get_results($wpdb->prepare(
"SELECT o.*, p.post_title as product_name,
(o.total * %f / 100) as platform_fee,
(o.total * (100 - %f) / 100) as creator_earning
FROM {$wpdb->prefix}wpdd_orders o
INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE p.post_author = %d
AND o.status = 'completed'
ORDER BY o.purchase_date DESC
LIMIT 100",
$commission_rate,
$commission_rate,
$user_id
));
// Get totals
$total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(o.total)
FROM {$wpdb->prefix}wpdd_orders o
INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE p.post_author = %d
AND o.status = 'completed'",
$user_id
));
$total_earnings = $total_sales * (1 - ($commission_rate / 100));
$current_balance = WPDD_Creator::get_creator_balance($user_id);
?>
<div class="wrap">
<h1><?php _e('My Sales Report', 'wp-digital-download'); ?></h1>
<div class="wpdd-stats-row" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Total Sales', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($total_sales, $currency); ?></div>
</div>
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Your Earnings', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($total_earnings, $currency); ?></div>
<small style="color: #646970;"><?php printf(__('After %s%% platform fee', 'wp-digital-download'), $commission_rate); ?></small>
</div>
<div class="wpdd-stat-card" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
<h3 style="margin: 0 0 10px; color: #646970;"><?php _e('Available Balance', 'wp-digital-download'); ?></h3>
<div style="font-size: 24px; font-weight: bold; color: #1d2327;"><?php echo wpdd_format_price($current_balance, $currency); ?></div>
<small style="color: #646970;"><?php _e('Ready for payout', 'wp-digital-download'); ?></small>
</div>
</div>
<?php if (!empty($sales)) : ?>
<div class="wpdd-sales-table">
<h2><?php _e('Recent Sales', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('Product', 'wp-digital-download'); ?></th>
<th><?php _e('Customer', 'wp-digital-download'); ?></th>
<th><?php _e('Sale Amount', 'wp-digital-download'); ?></th>
<th><?php _e('Platform Fee', 'wp-digital-download'); ?></th>
<th><?php _e('Your Earning', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($sales as $sale) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($sale->purchase_date))); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->customer_name); ?></td>
<td><?php echo wpdd_format_price($sale->total, $currency); ?></td>
<td><?php echo wpdd_format_price($sale->platform_fee, $currency); ?></td>
<td><strong><?php echo wpdd_format_price($sale->creator_earning, $currency); ?></strong></td>
<td>
<span class="wpdd-status-<?php echo esc_attr($sale->status); ?>" style="padding: 2px 8px; border-radius: 3px; font-size: 12px; <?php echo $sale->status === 'completed' ? 'background: #d1e7dd; color: #0f5132;' : 'background: #f8d7da; color: #721c24;'; ?>">
<?php echo esc_html(ucfirst($sale->status)); ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else : ?>
<div style="background: #fff; padding: 40px; border: 1px solid #ccd0d4; border-radius: 4px; text-align: center;">
<h3><?php _e('No sales yet', 'wp-digital-download'); ?></h3>
<p><?php _e('Once customers purchase your products, your sales data will appear here.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
</div>
<?php
}
public static function render_creator_payouts_page() {
global $wpdb;
if (isset($_POST['request_payout']) && wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_request_payout')) {
self::handle_payout_request();
}
$user_id = get_current_user_id();
$currency = get_option('wpdd_currency', 'USD');
$current_balance = WPDD_Creator::get_creator_balance($user_id);
$paypal_email = get_user_meta($user_id, 'wpdd_paypal_email', true);
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
// Get payout history
$payouts = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_payouts
WHERE creator_id = %d
ORDER BY created_at DESC
LIMIT 50",
$user_id
));
?>
<div class="wrap">
<h1><?php _e('My Payouts', 'wp-digital-download'); ?></h1>
<?php if (isset($_GET['message']) && $_GET['message'] === 'payout_requested') : ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Payout request submitted successfully!', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
<div class="wpdd-payout-request" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px; margin-bottom: 30px;">
<h2><?php _e('Request Payout', 'wp-digital-download'); ?></h2>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 20px; align-items: start;">
<div>
<h3><?php _e('Current Balance', 'wp-digital-download'); ?></h3>
<div style="font-size: 32px; font-weight: bold; color: #1d2327; margin: 10px 0;">
<?php echo wpdd_format_price($current_balance, $currency); ?>
</div>
<?php if ($threshold > 0) : ?>
<p style="color: #646970; margin: 0;">
<?php printf(__('Minimum for automatic payout: %s', 'wp-digital-download'), wpdd_format_price($threshold, $currency)); ?>
</p>
<?php endif; ?>
</div>
<div>
<?php if (empty($paypal_email)) : ?>
<div class="notice notice-warning" style="margin: 0;">
<p><?php _e('Please add your PayPal email in your profile before requesting a payout.', 'wp-digital-download'); ?></p>
<p><a href="<?php echo esc_url(get_edit_profile_url($user_id)); ?>" class="button"><?php _e('Edit Profile', 'wp-digital-download'); ?></a></p>
</div>
<?php elseif ($current_balance <= 0) : ?>
<div class="notice notice-info" style="margin: 0;">
<p><?php _e('No balance available for payout.', 'wp-digital-download'); ?></p>
</div>
<?php else : ?>
<form method="post">
<?php wp_nonce_field('wpdd_request_payout', 'wpdd_nonce'); ?>
<p><?php _e('PayPal Email:', 'wp-digital-download'); ?> <strong><?php echo esc_html($paypal_email); ?></strong></p>
<p><?php _e('Requesting a payout will notify administrators to process your payment.', 'wp-digital-download'); ?></p>
<button type="submit" name="request_payout" class="button button-primary">
<?php printf(__('Request Payout of %s', 'wp-digital-download'), wpdd_format_price($current_balance, $currency)); ?>
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($payouts)) : ?>
<div class="wpdd-payout-history">
<h2><?php _e('Payout History', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date Requested', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
<th><?php _e('Transaction ID', 'wp-digital-download'); ?></th>
<th><?php _e('Processed Date', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($payouts as $payout) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($payout->created_at))); ?></td>
<td><strong><?php echo wpdd_format_price($payout->amount, $payout->currency); ?></strong></td>
<td><?php echo esc_html($payout->paypal_email); ?></td>
<td>
<?php
$status_colors = array(
'pending' => '#fef3c7; color: #92400e;',
'completed' => '#d1fae5; color: #065f46;',
'failed' => '#fee2e2; color: #991b1b;',
'requested' => '#dbeafe; color: #1e40af;'
);
$status_color = isset($status_colors[$payout->status]) ? $status_colors[$payout->status] : '#f3f4f6; color: #374151;';
?>
<span style="padding: 2px 8px; border-radius: 3px; font-size: 12px; background: <?php echo $status_color; ?>">
<?php echo esc_html(ucfirst($payout->status)); ?>
</span>
</td>
<td><?php echo esc_html($payout->transaction_id ?: '-'); ?></td>
<td>
<?php
echo $payout->processed_at
? esc_html(date_i18n(get_option('date_format'), strtotime($payout->processed_at)))
: '-';
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else : ?>
<div style="background: #fff; padding: 40px; border: 1px solid #ccd0d4; border-radius: 4px; text-align: center;">
<h3><?php _e('No payout history', 'wp-digital-download'); ?></h3>
<p><?php _e('Your payout requests will appear here once you make them.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
</div>
<?php
}
private static function handle_payout_request() {
global $wpdb;
$user_id = get_current_user_id();
$balance = WPDD_Creator::get_creator_balance($user_id);
$paypal_email = get_user_meta($user_id, 'wpdd_paypal_email', true);
if ($balance <= 0 || empty($paypal_email)) {
return;
}
$currency = get_option('wpdd_currency', 'USD');
// Create payout request
$wpdb->insert(
$wpdb->prefix . 'wpdd_payouts',
array(
'creator_id' => $user_id,
'amount' => $balance,
'currency' => $currency,
'paypal_email' => $paypal_email,
'status' => 'requested',
'payout_method' => 'request',
'created_at' => current_time('mysql')
),
array('%d', '%f', '%s', '%s', '%s', '%s', '%s')
);
// Reset balance to 0 since it's now requested
update_user_meta($user_id, 'wpdd_creator_balance', 0);
// Redirect to avoid resubmission
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-creator-payouts&message=payout_requested'));
exit;
}
}

View File

@@ -16,7 +16,7 @@ class WPDD_Settings {
'edit.php?post_type=wpdd_product',
__('Settings', 'wp-digital-download'),
__('Settings', 'wp-digital-download'),
'wpdd_manage_settings',
'manage_options',
'wpdd-settings',
array(__CLASS__, 'render_settings_page')
);
@@ -26,9 +26,16 @@ class WPDD_Settings {
register_setting('wpdd_settings', 'wpdd_paypal_mode');
register_setting('wpdd_settings', 'wpdd_paypal_client_id');
register_setting('wpdd_settings', 'wpdd_paypal_secret');
register_setting('wpdd_settings', 'wpdd_paypal_payout_email');
register_setting('wpdd_settings', 'wpdd_admin_email');
register_setting('wpdd_settings', 'wpdd_from_name');
register_setting('wpdd_settings', 'wpdd_from_email');
register_setting('wpdd_settings', 'wpdd_smtp_enabled');
register_setting('wpdd_settings', 'wpdd_smtp_host');
register_setting('wpdd_settings', 'wpdd_smtp_port');
register_setting('wpdd_settings', 'wpdd_smtp_username');
register_setting('wpdd_settings', 'wpdd_smtp_password');
register_setting('wpdd_settings', 'wpdd_smtp_encryption');
register_setting('wpdd_settings', 'wpdd_currency');
register_setting('wpdd_settings', 'wpdd_enable_guest_checkout');
register_setting('wpdd_settings', 'wpdd_default_download_limit');
@@ -193,6 +200,18 @@ class WPDD_Settings {
'wpdd_paypal_settings',
array('name' => 'wpdd_paypal_secret')
);
add_settings_field(
'wpdd_paypal_payout_email',
__('PayPal Payout Account Email', 'wp-digital-download'),
array(__CLASS__, 'email_field'),
'wpdd_settings',
'wpdd_paypal_settings',
array(
'name' => 'wpdd_paypal_payout_email',
'description' => __('PayPal account email that will send payouts to creators', 'wp-digital-download')
)
);
}
private static function add_email_fields() {
@@ -231,6 +250,94 @@ class WPDD_Settings {
'description' => __('Email address shown in email headers', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_smtp_enabled',
__('Enable SMTP', 'wp-digital-download'),
array(__CLASS__, 'checkbox_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_enabled',
'label' => __('Use SMTP for sending emails instead of PHP mail()', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_smtp_host',
__('SMTP Host', 'wp-digital-download'),
array(__CLASS__, 'text_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_host',
'description' => __('SMTP server hostname (e.g., smtp.gmail.com)', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_smtp_port',
__('SMTP Port', 'wp-digital-download'),
array(__CLASS__, 'number_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_port',
'description' => __('SMTP server port number (common ports: 25, 465, 587)', 'wp-digital-download'),
'min' => 1,
'max' => 65535
)
);
add_settings_field(
'wpdd_smtp_encryption',
__('SMTP Encryption', 'wp-digital-download'),
array(__CLASS__, 'select_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_encryption',
'options' => array(
'' => __('None', 'wp-digital-download'),
'tls' => __('TLS', 'wp-digital-download'),
'ssl' => __('SSL', 'wp-digital-download')
),
'description' => __('Select encryption method - TLS is recommended for most providers', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_smtp_autodetect',
__('Auto-Detect Settings', 'wp-digital-download'),
array(__CLASS__, 'smtp_autodetect_field'),
'wpdd_settings',
'wpdd_email_settings',
array()
);
add_settings_field(
'wpdd_smtp_username',
__('SMTP Username', 'wp-digital-download'),
array(__CLASS__, 'text_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_username',
'description' => __('SMTP authentication username', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_smtp_password',
__('SMTP Password', 'wp-digital-download'),
array(__CLASS__, 'password_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_smtp_password',
'description' => __('SMTP authentication password', 'wp-digital-download')
)
);
}
private static function add_download_fields() {
@@ -305,16 +412,62 @@ class WPDD_Settings {
}
public static function render_settings_page() {
$active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'general';
?>
<div class="wrap">
<h1><?php _e('WP Digital Download Settings', 'wp-digital-download'); ?></h1>
<h2 class="nav-tab-wrapper">
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=general'); ?>"
class="nav-tab <?php echo $active_tab == 'general' ? 'nav-tab-active' : ''; ?>"><?php _e('General', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=paypal'); ?>"
class="nav-tab <?php echo $active_tab == 'paypal' ? 'nav-tab-active' : ''; ?>"><?php _e('PayPal', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=email'); ?>"
class="nav-tab <?php echo $active_tab == 'email' ? 'nav-tab-active' : ''; ?>"><?php _e('Email', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=downloads'); ?>"
class="nav-tab <?php echo $active_tab == 'downloads' ? 'nav-tab-active' : ''; ?>"><?php _e('Downloads', 'wp-digital-download'); ?></a>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-settings&tab=watermark'); ?>"
class="nav-tab <?php echo $active_tab == 'watermark' ? 'nav-tab-active' : ''; ?>"><?php _e('Watermark', 'wp-digital-download'); ?></a>
</h2>
<div class="wpdd-settings-container">
<div class="wpdd-settings-content">
<form method="post" action="options.php" class="wpdd-settings-form">
<?php
settings_fields('wpdd_settings');
switch ($active_tab) {
case 'general':
self::render_general_tab();
break;
case 'paypal':
self::render_paypal_tab();
break;
case 'email':
self::render_email_tab();
break;
case 'downloads':
self::render_downloads_tab();
break;
case 'watermark':
self::render_watermark_tab();
break;
default:
self::render_general_tab();
}
submit_button();
?>
</form>
</div>
<div class="wpdd-settings-sidebar">
<?php if ($active_tab == 'general') : ?>
<div class="wpdd-settings-box">
<h3><?php _e('Quick Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('To get started quickly:', 'wp-digital-download'); ?></p>
<ol>
<li><?php _e('Configure PayPal settings above', 'wp-digital-download'); ?></li>
<li><?php _e('Configure PayPal settings', 'wp-digital-download'); ?></li>
<li><?php _e('Create your first product', 'wp-digital-download'); ?></li>
<li><?php _e('Add the shop shortcode [wpdd_shop] to a page', 'wp-digital-download'); ?></li>
<li><?php _e('Test with a free product first', 'wp-digital-download'); ?></li>
@@ -331,33 +484,29 @@ class WPDD_Settings {
<li><code>[wpdd_product id="123"]</code> - <?php _e('Single product display', 'wp-digital-download'); ?></li>
</ul>
</div>
<?php endif; ?>
<div class="wpdd-settings-box">
<h3><?php _e('System Status', 'wp-digital-download'); ?></h3>
<?php self::system_status(); ?>
</div>
</div>
<form method="post" action="options.php" class="wpdd-settings-form">
<?php
settings_fields('wpdd_settings');
do_settings_sections('wpdd_settings');
submit_button();
?>
</form>
</div>
</div>
<style>
.wpdd-settings-sidebar {
float: right;
width: 300px;
margin-left: 20px;
position: relative;
z-index: 10;
.wpdd-settings-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
.wpdd-settings-form {
overflow: hidden;
margin-right: 340px;
.wpdd-settings-content {
flex: 1;
min-width: 0;
}
.wpdd-settings-sidebar {
width: 300px;
flex-shrink: 0;
}
.wpdd-settings-box {
background: white;
@@ -377,15 +526,21 @@ class WPDD_Settings {
.wpdd-status-good { color: #46b450; }
.wpdd-status-warning { color: #ffb900; }
.wpdd-status-error { color: #dc3232; }
@media (max-width: 1200px) {
.wpdd-settings-sidebar {
float: none;
width: 100%;
margin-left: 0;
margin-top: 30px;
.wpdd-tab-content {
background: white;
border: 1px solid #ccd0d4;
padding: 20px;
margin-top: -1px;
}
.wpdd-settings-form {
margin-right: 0;
@media (max-width: 1200px) {
.wpdd-settings-container {
flex-direction: column;
}
.wpdd-settings-sidebar {
width: 100%;
order: 2;
}
}
</style>
@@ -584,9 +739,98 @@ class WPDD_Settings {
));
}
public static function smtp_autodetect_field($args) {
?>
<button type="button" id="wpdd-smtp-autodetect" class="button button-secondary">
<?php _e('Auto-Detect SMTP Settings', 'wp-digital-download'); ?>
</button>
<span id="wpdd-smtp-autodetect-status" style="margin-left: 10px;"></span>
<p class="description">
<?php _e('Automatically detect port and encryption settings based on the SMTP host.', 'wp-digital-download'); ?>
</p>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#wpdd-smtp-autodetect').on('click', function() {
var $button = $(this);
var $status = $('#wpdd-smtp-autodetect-status');
var host = $('#wpdd_smtp_host').val();
if (!host) {
$status.html('<span style="color: #dc3232;"><?php _e('Please enter SMTP host first.', 'wp-digital-download'); ?></span>');
return;
}
$button.prop('disabled', true).text('<?php _e('Detecting...', 'wp-digital-download'); ?>');
$status.html('<span style="color: #0073aa;"><?php _e('Testing connection...', 'wp-digital-download'); ?></span>');
// Test common SMTP configurations
var configs = [
{ port: 587, encryption: 'tls' },
{ port: 465, encryption: 'ssl' },
{ port: 25, encryption: 'tls' },
{ port: 25, encryption: '' }
];
// Detect common providers
var detectedConfig = null;
var hostname = host.toLowerCase();
if (hostname.includes('gmail.com') || hostname.includes('google.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('outlook.com') || hostname.includes('hotmail.com') || hostname.includes('live.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('yahoo.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else if (hostname.includes('smtp.') && hostname.includes('.com')) {
detectedConfig = { port: 587, encryption: 'tls' };
} else {
// Default to most common configuration
detectedConfig = { port: 587, encryption: 'tls' };
}
// Apply detected settings
setTimeout(function() {
$('#wpdd_smtp_port').val(detectedConfig.port);
$('#wpdd_smtp_encryption').val(detectedConfig.encryption);
$button.prop('disabled', false).text('<?php _e('Auto-Detect SMTP Settings', 'wp-digital-download'); ?>');
$status.html('<span style="color: #46b450;"><?php _e('Settings detected and applied!', 'wp-digital-download'); ?></span>');
// Clear status after 5 seconds
setTimeout(function() {
$status.html('');
}, 5000);
}, 1000);
});
});
</script>
<?php
}
private static function system_status() {
$status = array();
// Check if WPDD_UPLOADS_DIR is defined to prevent fatal errors
if (!defined('WPDD_UPLOADS_DIR')) {
$status[] = array(
'label' => __('Plugin Constants', 'wp-digital-download'),
'value' => __('Not Loaded', 'wp-digital-download'),
'class' => 'wpdd-status-error'
);
echo '<ul>';
foreach ($status as $item) {
printf(
'<li>%s: <span class="%s">%s</span></li>',
esc_html($item['label']),
esc_attr($item['class']),
esc_html($item['value'])
);
}
echo '</ul>';
return;
}
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
@@ -659,6 +903,73 @@ class WPDD_Settings {
echo '</ul>';
}
public static function render_general_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('General Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_general_settings'); ?>
</div>
<?php
}
public static function render_paypal_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('PayPal Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_paypal_settings'); ?>
</div>
<?php
}
public static function render_email_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Email Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_email_settings'); ?>
</div>
<?php
}
public static function render_downloads_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Download Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_download_settings'); ?>
</div>
<?php
}
public static function render_watermark_tab() {
?>
<div class="wpdd-tab-content">
<h2><?php _e('Watermark Settings', 'wp-digital-download'); ?></h2>
<?php self::do_settings_sections_for_tab('wpdd_watermark_settings'); ?>
</div>
<?php
}
private static function do_settings_sections_for_tab($section_id) {
global $wp_settings_sections, $wp_settings_fields;
if (!isset($wp_settings_sections['wpdd_settings'][$section_id])) {
return;
}
$section = $wp_settings_sections['wpdd_settings'][$section_id];
if (isset($section['callback']) && $section['callback']) {
call_user_func($section['callback'], $section);
}
if (!isset($wp_settings_fields['wpdd_settings'][$section_id])) {
return;
}
echo '<table class="form-table" role="presentation">';
do_settings_fields('wpdd_settings', $section_id);
echo '</table>';
}
public static function sanitize_commission_rate($input) {
$value = floatval($input);
if ($value < 0) {

View File

@@ -0,0 +1,261 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
// $order variable is passed from the calling function
?>
<div class="wrap">
<h1><?php _e('Order Details', 'wp-digital-download'); ?></h1>
<div class="order-details-container" style="max-width: 800px;">
<div class="order-summary" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Order Summary', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Order Number', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->order_number); ?></td>
</tr>
<tr>
<td><strong><?php _e('Status', 'wp-digital-download'); ?></strong></td>
<td>
<span class="order-status status-<?php echo esc_attr($order->status); ?>" style="padding: 4px 8px; border-radius: 3px; font-size: 12px; <?php
echo $order->status === 'completed' ? 'background: #d1e7dd; color: #0f5132;' :
($order->status === 'pending' ? 'background: #fff3cd; color: #856404;' :
'background: #f8d7da; color: #721c24;');
?>">
<?php echo esc_html(ucfirst($order->status)); ?>
</span>
</td>
</tr>
<tr>
<td><strong><?php _e('Purchase Date', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($order->purchase_date))); ?></td>
</tr>
<tr>
<td><strong><?php _e('Payment Method', 'wp-digital-download'); ?></strong></td>
<td>
<?php
$payment_methods = array(
'paypal' => 'PayPal',
'free' => 'Free Download',
'manual' => 'Manual Payment'
);
echo esc_html($payment_methods[$order->payment_method] ?? ucfirst($order->payment_method));
?>
</td>
</tr>
<?php if (!empty($order->transaction_id)) : ?>
<tr>
<td><strong><?php _e('Transaction ID', 'wp-digital-download'); ?></strong></td>
<td><code><?php echo esc_html($order->transaction_id); ?></code></td>
</tr>
<?php endif; ?>
<tr>
<td><strong><?php _e('Amount', 'wp-digital-download'); ?></strong></td>
<td>
<strong style="font-size: 16px;">
<?php echo wpdd_format_price($order->amount, $order->currency); ?>
</strong>
</td>
</tr>
</tbody>
</table>
</div>
<div class="customer-details" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Customer Information', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Customer Name', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->customer_name); ?></td>
</tr>
<tr>
<td><strong><?php _e('Email Address', 'wp-digital-download'); ?></strong></td>
<td>
<a href="mailto:<?php echo esc_attr($order->customer_email); ?>">
<?php echo esc_html($order->customer_email); ?>
</a>
</td>
</tr>
<?php if ($order->customer_id > 0) : ?>
<tr>
<td><strong><?php _e('WordPress User', 'wp-digital-download'); ?></strong></td>
<td>
<a href="<?php echo admin_url('user-edit.php?user_id=' . $order->customer_id); ?>">
<?php _e('View User Profile', 'wp-digital-download'); ?>
</a>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="product-details" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Product Information', 'wp-digital-download'); ?></h2>
<table class="widefat fixed">
<tbody>
<tr>
<td style="width: 200px;"><strong><?php _e('Product', 'wp-digital-download'); ?></strong></td>
<td>
<a href="<?php echo admin_url('post.php?post=' . $order->product_id . '&action=edit'); ?>">
<?php echo esc_html($order->product_name); ?>
</a>
</td>
</tr>
<tr>
<td><strong><?php _e('Product ID', 'wp-digital-download'); ?></strong></td>
<td><?php echo esc_html($order->product_id); ?></td>
</tr>
<?php if ($order->creator_id > 0) : ?>
<tr>
<td><strong><?php _e('Creator', 'wp-digital-download'); ?></strong></td>
<td>
<?php
$creator = get_userdata($order->creator_id);
if ($creator) : ?>
<a href="<?php echo admin_url('user-edit.php?user_id=' . $order->creator_id); ?>">
<?php echo esc_html($creator->display_name); ?>
</a>
<?php else : ?>
<?php _e('Creator not found', 'wp-digital-download'); ?>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// Get download links for this order
global $wpdb;
$download_links = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_download_links WHERE order_id = %d ORDER BY created_at DESC",
$order->id
));
?>
<?php if (!empty($download_links)) : ?>
<div class="download-links" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Download Links', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Download Token', 'wp-digital-download'); ?></th>
<th><?php _e('Downloads', 'wp-digital-download'); ?></th>
<th><?php _e('Expires', 'wp-digital-download'); ?></th>
<th><?php _e('Created', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($download_links as $link) :
$is_expired = strtotime($link->expires_at) < current_time('timestamp');
$is_used_up = $link->download_count >= $link->max_downloads;
?>
<tr>
<td>
<code style="font-size: 11px;"><?php echo esc_html(substr($link->token, 0, 20)) . '...'; ?></code>
</td>
<td>
<?php echo esc_html($link->download_count); ?> / <?php echo esc_html($link->max_downloads); ?>
</td>
<td>
<?php
if ($is_expired) : ?>
<span style="color: #d63384;"><?php _e('Expired', 'wp-digital-download'); ?></span>
<?php else : ?>
<?php echo esc_html(date_i18n(get_option('date_format'), strtotime($link->expires_at))); ?>
<?php endif; ?>
</td>
<td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($link->created_at))); ?></td>
<td>
<?php if ($is_expired) : ?>
<span style="color: #d63384;"><?php _e('Expired', 'wp-digital-download'); ?></span>
<?php elseif ($is_used_up) : ?>
<span style="color: #fd7e14;"><?php _e('Used Up', 'wp-digital-download'); ?></span>
<?php else : ?>
<span style="color: #198754;"><?php _e('Active', 'wp-digital-download'); ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php
// Get download history for this order
$downloads = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_downloads WHERE order_id = %d ORDER BY download_date DESC LIMIT 20",
$order->id
));
?>
<?php if (!empty($downloads)) : ?>
<div class="download-history" style="background: #fff; border: 1px solid #ccd0d4; padding: 20px; margin-bottom: 20px;">
<h2><?php _e('Download History', 'wp-digital-download'); ?>
<small>(<?php printf(__('Last %d downloads', 'wp-digital-download'), count($downloads)); ?>)</small>
</h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('IP Address', 'wp-digital-download'); ?></th>
<th><?php _e('User Agent', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($downloads as $download) : ?>
<tr>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($download->download_date))); ?></td>
<td><code><?php echo esc_html($download->ip_address); ?></code></td>
<td style="font-size: 11px;">
<?php echo esc_html(wp_trim_words($download->user_agent, 10)); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<div class="order-actions" style="margin-top: 30px;">
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-orders'); ?>" class="button">
<?php _e('← Back to Orders', 'wp-digital-download'); ?>
</a>
<?php if ($order->status === 'completed') : ?>
<a href="mailto:<?php echo esc_attr($order->customer_email); ?>?subject=<?php echo urlencode('Your Order: ' . $order->order_number); ?>" class="button button-secondary">
<?php _e('Email Customer', 'wp-digital-download'); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
<style>
.order-details-container table.widefat td {
padding: 12px;
border-bottom: 1px solid #f0f0f1;
}
.order-details-container table.widefat td:first-child {
background-color: #f6f7f7;
}
.order-status {
font-weight: bold;
text-transform: uppercase;
}
</style>

124
assets/js/admin-payouts.js Normal file
View File

@@ -0,0 +1,124 @@
jQuery(document).ready(function($) {
// Handle individual payout processing
$('.wpdd-process-payout').on('click', function(e) {
e.preventDefault();
var $button = $(this);
var payoutId = $button.data('payout-id');
var originalText = $button.text();
// Disable button and show loading
$button.prop('disabled', true).text('Processing...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_process_payout',
payout_id: payoutId,
nonce: wpdd_admin_nonce
},
success: function(response) {
if (response.success) {
$button.closest('tr').find('.payout-status').text('Processing');
$button.text('Processed').removeClass('wpdd-process-payout').addClass('button-disabled');
} else {
alert('Error: ' + (response.data || 'Unknown error occurred'));
$button.prop('disabled', false).text(originalText);
}
},
error: function() {
alert('Ajax request failed');
$button.prop('disabled', false).text(originalText);
}
});
});
// Handle payout request approval
$('.wpdd-approve-request').on('click', function(e) {
e.preventDefault();
var $button = $(this);
var requestId = $button.data('request-id');
var originalText = $button.text();
// Disable button and show loading
$button.prop('disabled', true).text('Processing...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_approve_payout_request',
request_id: requestId,
nonce: wpdd_admin_nonce
},
success: function(response) {
if (response.success) {
location.reload(); // Reload to show updated status
} else {
alert('Error: ' + (response.data || 'Unknown error occurred'));
$button.prop('disabled', false).text(originalText);
}
},
error: function() {
alert('Ajax request failed');
$button.prop('disabled', false).text(originalText);
}
});
});
// Handle bulk payout processing
$('#wpdd-process-bulk-payouts').on('click', function(e) {
e.preventDefault();
var checkedPayouts = $('.wpdd-payout-checkbox:checked');
if (checkedPayouts.length === 0) {
alert('Please select at least one payout to process.');
return;
}
if (!confirm('Are you sure you want to process ' + checkedPayouts.length + ' payout(s)?')) {
return;
}
var $button = $(this);
var originalText = $button.text();
var payoutIds = [];
checkedPayouts.each(function() {
payoutIds.push($(this).val());
});
// Disable button and show loading
$button.prop('disabled', true).text('Processing...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_process_bulk_payouts',
payout_ids: payoutIds,
nonce: wpdd_admin_nonce
},
success: function(response) {
if (response.success) {
location.reload(); // Reload to show updated status
} else {
alert('Error: ' + (response.data || 'Unknown error occurred'));
$button.prop('disabled', false).text(originalText);
}
},
error: function() {
alert('Ajax request failed');
$button.prop('disabled', false).text(originalText);
}
});
});
// Handle select all checkboxes
$('#wpdd-select-all-payouts').on('change', function() {
$('.wpdd-payout-checkbox').prop('checked', $(this).prop('checked'));
});
});

View File

@@ -0,0 +1,420 @@
# WP Digital Download - Software Licensing Integration Guide
This guide shows developers how to integrate their WordPress plugins with the WP Digital Download licensing and update system.
## Quick Start
### 1. Download the Integration Library
Download `wpdd-plugin-updater.php` from your product page and include it in your plugin.
### 2. Basic Integration
```php
<?php
/**
* Plugin Name: My Awesome Plugin
* Version: 1.0.0
*/
// Include the WPDD updater library
require_once plugin_dir_path(__FILE__) . 'includes/wpdd-plugin-updater.php';
class My_Awesome_Plugin {
private $updater;
public function __construct() {
$this->init_updater();
}
private function init_updater() {
// Only initialize updater in admin area
if (!is_admin()) {
return;
}
$license_key = get_option('my_plugin_license_key', '');
$this->updater = new WPDD_Plugin_Updater(
__FILE__, // Main plugin file
$license_key, // License key from user
'https://your-store.com', // Your store URL
array(
'add_settings_page' => true // Add license settings page
)
);
}
}
new My_Awesome_Plugin();
```
## Advanced Integration
### Custom License Settings Page
If you want to integrate license management into your existing settings:
```php
class My_Plugin_Settings {
private $updater;
public function __construct() {
$this->init_updater();
add_action('admin_menu', array($this, 'add_settings_page'));
add_action('admin_init', array($this, 'handle_license_actions'));
}
private function init_updater() {
$license_key = get_option('my_plugin_license_key', '');
$this->updater = new WPDD_Plugin_Updater(
MY_PLUGIN_FILE,
$license_key,
'https://your-store.com',
array('add_settings_page' => false) // We'll handle settings ourselves
);
}
public function handle_license_actions() {
if (isset($_POST['activate_license'])) {
$license_key = sanitize_text_field($_POST['license_key']);
$result = $this->updater->activate_license($license_key);
if ($result['success']) {
update_option('my_plugin_license_key', $license_key);
add_settings_error('my_plugin', 'activated', 'License activated!', 'updated');
} else {
add_settings_error('my_plugin', 'error', $result['message'], 'error');
}
}
if (isset($_POST['deactivate_license'])) {
$result = $this->updater->deactivate_license();
if ($result['success']) {
delete_option('my_plugin_license_key');
add_settings_error('my_plugin', 'deactivated', 'License deactivated!', 'updated');
}
}
}
public function render_license_section() {
$license_key = get_option('my_plugin_license_key', '');
$is_valid = $this->updater->validate_license();
?>
<h3>License Settings</h3>
<table class="form-table">
<tr>
<th><label for="license_key">License Key</label></th>
<td>
<input type="text" id="license_key" name="license_key"
value="<?php echo esc_attr($license_key); ?>" class="regular-text" />
<?php if ($is_valid): ?>
<span style="color: green;">✓ Active</span>
<?php elseif (!empty($license_key)): ?>
<span style="color: red;">✗ Invalid</span>
<?php endif; ?>
<p class="description">
Enter your license key to receive automatic updates.
</p>
</td>
</tr>
</table>
<?php if (empty($license_key) || !$is_valid): ?>
<p>
<input type="submit" name="activate_license" class="button-primary" value="Activate License" />
</p>
<?php else: ?>
<p>
<input type="submit" name="deactivate_license" class="button-secondary" value="Deactivate License" />
</p>
<?php endif; ?>
<?php
}
}
```
### Manual License Validation
For premium features or activation checks:
```php
class My_Premium_Feature {
private $updater;
public function __construct() {
$license_key = get_option('my_plugin_license_key', '');
$this->updater = new WPDD_Plugin_Updater(
MY_PLUGIN_FILE,
$license_key,
'https://your-store.com'
);
// Only enable premium features if license is valid
if ($this->is_license_valid()) {
$this->enable_premium_features();
} else {
$this->show_license_notice();
}
}
private function is_license_valid() {
return $this->updater->validate_license();
}
private function enable_premium_features() {
// Add your premium functionality here
add_action('init', array($this, 'init_premium_features'));
}
private function show_license_notice() {
add_action('admin_notices', function() {
?>
<div class="notice notice-warning">
<p>
<strong>My Awesome Plugin:</strong>
Please <a href="<?php echo admin_url('options-general.php?page=my-plugin-settings'); ?>">
activate your license</a> to access premium features and receive updates.
</p>
</div>
<?php
});
}
}
```
## API Reference
### WPDD_Plugin_Updater Class
#### Constructor Parameters
```php
new WPDD_Plugin_Updater($plugin_file, $license_key, $update_server, $args);
```
- **$plugin_file** (string) - Full path to your main plugin file
- **$license_key** (string) - The user's license key
- **$update_server** (string) - URL to your store (e.g., 'https://your-store.com')
- **$args** (array) - Optional arguments:
- `add_settings_page` (bool) - Auto-create license settings page (default: false)
#### Methods
##### validate_license()
Validates the current license with the server.
```php
$is_valid = $updater->validate_license();
// Returns: boolean
```
##### activate_license($license_key)
Activates a license key for the current site.
```php
$result = $updater->activate_license('XXXX-XXXX-XXXX-XXXX');
// Returns: array with 'success', 'message', and additional data
```
##### deactivate_license()
Deactivates the current license from this site.
```php
$result = $updater->deactivate_license();
// Returns: array with 'success' and 'message'
```
## Repository Setup (For Store Owners)
### 1. Create Software Product
1. Go to your WordPress admin → Digital Products → Add New Product
2. Select "Software License" as product type
3. Fill in the software licensing fields:
- Git Repository URL
- License settings (max activations, duration)
- Version information
### 2. Configure Git Webhook
Add the generated webhook URL to your repository settings. The system receives webhook notifications FROM your Git platform when releases are published:
**Gitea:**
1. Go to Settings → Webhooks
2. Add webhook with the URL from your product page
3. Set Content-Type to `application/json`
4. Select "Release events" as the trigger
5. Ensure webhook is active
**GitHub:**
1. Go to Settings → Webhooks
2. Add webhook with the URL from your product page
3. Set Content-Type to `application/json`
4. Select "Releases" events (or "Just the push event" for tag-based releases)
**GitLab:**
1. Go to Settings → Webhooks
2. Add the webhook URL
3. Select "Tag push events" or "Releases events"
### 3. Release Process
**Option 1: Using Git Platform Releases (Recommended for Gitea/GitHub)**
1. Create a release through your Git platform's web interface:
- Navigate to Releases section
- Click "Create Release" or "New Release"
- Set tag name (e.g., `v1.2.0`)
- Add release notes in the description
- Publish the release
2. The webhook automatically receives the release notification and:
- Detects the new version from the release
- Clones the repository at the specific tag
- Creates distribution packages (removes dev files, creates ZIP)
- Stores version info and changelog in the database
- Makes update available to customers with active licenses
**Option 2: Using Git Tags (Alternative)**
1. Create and push a git tag:
```bash
git tag -a v1.2.0 -m "Version 1.2.0"
git push origin v1.2.0
```
2. The webhook receives the tag push notification and processes the release similarly
## API Endpoints
### License Validation
```
POST /wp-json/wpdd/v1/validate-license
Body: {
"license_key": "XXXX-XXXX-XXXX-XXXX",
"product_slug": "my-plugin",
"site_url": "https://example.com"
}
```
### Update Check
```
GET /wp-json/wpdd/v1/check-update/my-plugin?license_key=XXXX&version=1.0.0
```
### Download Update
```
GET /wp-json/wpdd/v1/download-update/my-plugin?license_key=XXXX
```
## Testing Your Integration
### 1. Local Testing
```php
// Add this to your plugin for testing
if (defined('WP_DEBUG') && WP_DEBUG) {
add_action('admin_notices', function() {
$license_key = get_option('my_plugin_license_key', '');
$updater = new WPDD_Plugin_Updater(__FILE__, $license_key, 'https://your-store.com');
$is_valid = $updater->validate_license();
echo '<div class="notice notice-info">';
echo '<p>License Status: ' . ($is_valid ? 'Valid' : 'Invalid') . '</p>';
echo '</div>';
});
}
```
### 2. Force Update Check
```php
// Add this temporarily to force update check
add_action('admin_init', function() {
if (isset($_GET['force_update_check'])) {
delete_transient('wpdd_update_my-plugin');
delete_site_transient('update_plugins');
wp_redirect(admin_url('plugins.php'));
exit;
}
});
```
Then visit: `wp-admin/plugins.php?force_update_check=1`
## Best Practices
### 1. Error Handling
Always handle API failures gracefully:
```php
$result = $updater->validate_license();
if ($result === false) {
// Network error or server down - allow functionality to continue
// but maybe show a notice
}
```
### 2. Caching
The updater automatically caches responses. Don't call validation on every page load:
```php
// Good - check once per day
$last_check = get_option('my_plugin_license_check', 0);
if (time() - $last_check > DAY_IN_SECONDS) {
$is_valid = $updater->validate_license();
update_option('my_plugin_license_check', time());
update_option('my_plugin_license_valid', $is_valid);
} else {
$is_valid = get_option('my_plugin_license_valid', false);
}
```
### 3. Graceful Degradation
Design your plugin to work without a valid license, but with reduced functionality:
```php
if ($this->is_license_valid()) {
// Full functionality
$this->enable_all_features();
} else {
// Basic functionality only
$this->enable_basic_features();
$this->show_upgrade_notice();
}
```
## Troubleshooting
### Common Issues
1. **Updates not showing:** Check that the plugin slug matches the product slug in your store
2. **License validation fails:** Ensure the update server URL is correct and accessible
3. **Download fails:** Verify the license is activated and not expired
### Debug Mode
Enable WordPress debug logging and check for WPDD Updater messages:
```php
// wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
```
Check `/wp-content/debug.log` for error messages.
## Support
For integration support:
- Check the troubleshooting section above
- Enable debug logging and check for error messages
- Contact support with your store URL and plugin details
## Example Files
Complete example plugins are available in the `/examples/` directory of this package.

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

View File

@@ -39,6 +39,7 @@ final class WP_Digital_Download {
private function load_dependencies() {
$files = array(
'includes/class-wpdd-install.php',
'includes/class-wpdd-setup.php',
'includes/class-wpdd-post-types.php',
'includes/class-wpdd-roles.php',
'includes/class-wpdd-metaboxes.php',
@@ -46,6 +47,8 @@ final class WP_Digital_Download {
'includes/class-wpdd-paypal.php',
'includes/class-wpdd-download-handler.php',
'includes/class-wpdd-customer.php',
'includes/class-wpdd-license-manager.php',
'includes/class-wpdd-api.php',
'includes/class-wpdd-orders.php',
'includes/class-wpdd-file-protection.php',
'includes/class-wpdd-watermark.php',
@@ -168,7 +171,25 @@ final class WP_Digital_Download {
error_log('WPDD Error: WPDD_Ajax class not found');
}
if (class_exists('WPDD_License_Manager')) {
WPDD_License_Manager::init();
} else {
error_log('WPDD Error: WPDD_License_Manager class not found');
}
if (class_exists('WPDD_API')) {
WPDD_API::init();
} else {
error_log('WPDD Error: WPDD_API class not found');
}
if (is_admin()) {
if (class_exists('WPDD_Setup')) {
WPDD_Setup::init();
} else {
error_log('WPDD Error: WPDD_Setup class not found');
}
if (class_exists('WPDD_Admin')) {
WPDD_Admin::init();
} else {