Adding more functionality
This commit is contained in:
BIN
.playwright-mcp/page-2025-08-29T02-49-20-380Z.png
Normal file
BIN
.playwright-mcp/page-2025-08-29T02-49-20-380Z.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
@@ -140,3 +140,5 @@ Plugin automatically includes `wpdd_product` post type in WordPress search resul
|
||||
These are sandbox only PayPal Test account. The credentials should be used to test the purchase process.
|
||||
- sb-a7cpw45634739@personal.example.com
|
||||
- 3[I$ppb?
|
||||
- When copying files to ~/remote-sftp you MUST check if the file "THIS-IS-REMOTE" exists. If it does not exist, tell the user they need to mount the remote path.
|
||||
- When using the playwright MCP, please use headless if possible.
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
261
admin/views/order-details.php
Normal file
261
admin/views/order-details.php
Normal 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
124
assets/js/admin-payouts.js
Normal 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'));
|
||||
});
|
||||
});
|
420
docs/developer-integration-guide.md
Normal file
420
docs/developer-integration-guide.md
Normal 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
785
includes/class-wpdd-api.php
Normal 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
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
|
399
includes/class-wpdd-license-manager.php
Normal file
399
includes/class-wpdd-license-manager.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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']));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
169
includes/class-wpdd-setup.php
Normal file
169
includes/class-wpdd-setup.php
Normal 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();
|
||||
}
|
||||
}
|
471
includes/wpdd-plugin-updater.php
Normal file
471
includes/wpdd-plugin-updater.php
Normal 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
|
136
tests/webhook-samples.md
Normal file
136
tests/webhook-samples.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Gitea Webhook Integration Test Samples
|
||||
|
||||
The corrected webhook implementation now properly receives and processes webhook notifications FROM Git platforms like Gitea when new releases are published.
|
||||
|
||||
## Sample Gitea Release Webhook Payload
|
||||
|
||||
When a release is published in Gitea, it sends a webhook payload like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "published",
|
||||
"release": {
|
||||
"id": 123,
|
||||
"tag_name": "v1.2.0",
|
||||
"target_commitish": "main",
|
||||
"name": "Version 1.2.0",
|
||||
"body": "## What's New\n- Added new feature X\n- Fixed bug Y\n- Improved performance\n\n## Breaking Changes\n- None",
|
||||
"url": "https://git.example.com/user/repo/releases/tag/v1.2.0",
|
||||
"html_url": "https://git.example.com/user/repo/releases/tag/v1.2.0",
|
||||
"tarball_url": "https://git.example.com/user/repo/archive/v1.2.0.tar.gz",
|
||||
"zipball_url": "https://git.example.com/user/repo/archive/v1.2.0.zip",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
"created_at": "2025-01-15T10:30:00Z",
|
||||
"published_at": "2025-01-15T10:30:00Z",
|
||||
"author": {
|
||||
"id": 1,
|
||||
"login": "developer",
|
||||
"full_name": "Developer Name"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"id": 456,
|
||||
"name": "my-wordpress-plugin",
|
||||
"full_name": "user/my-wordpress-plugin",
|
||||
"html_url": "https://git.example.com/user/my-wordpress-plugin",
|
||||
"clone_url": "https://git.example.com/user/my-wordpress-plugin.git"
|
||||
},
|
||||
"sender": {
|
||||
"id": 1,
|
||||
"login": "developer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How the Webhook Handler Processes This
|
||||
|
||||
1. **Authentication**: Validates the passcode in the URL path
|
||||
2. **Event Detection**: Identifies this as a Gitea release event (`action: "published"`)
|
||||
3. **Version Extraction**: Extracts version "1.2.0" from `tag_name: "v1.2.0"`
|
||||
4. **Changelog Processing**: Uses the release `body` field for changelog
|
||||
5. **Package Creation**: Clones the repository at tag `v1.2.0` and creates distribution package
|
||||
6. **Database Storage**: Stores the new version in `wpdd_software_versions` table
|
||||
7. **Customer Updates**: Customers with valid licenses can now receive the update
|
||||
|
||||
## Sample GitHub Release Webhook Payload
|
||||
|
||||
GitHub uses a similar but slightly different structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "published",
|
||||
"release": {
|
||||
"tag_name": "v1.2.0",
|
||||
"target_commitish": "main",
|
||||
"name": "Version 1.2.0",
|
||||
"body": "Release notes here...",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
},
|
||||
"repository": {
|
||||
"name": "my-plugin",
|
||||
"clone_url": "https://github.com/user/my-plugin.git"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sample Git Push with Tag Webhook
|
||||
|
||||
For platforms that send tag push events instead of release events:
|
||||
|
||||
```json
|
||||
{
|
||||
"ref": "refs/tags/v1.2.0",
|
||||
"repository": {
|
||||
"clone_url": "https://git.example.com/user/repo.git"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "abc123",
|
||||
"message": "Release version 1.2.0"
|
||||
}
|
||||
],
|
||||
"head_commit": {
|
||||
"id": "abc123",
|
||||
"message": "Release version 1.2.0"
|
||||
},
|
||||
"after": "abc123"
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook URL Format
|
||||
|
||||
The webhook URLs generated for each software product follow this format:
|
||||
|
||||
```
|
||||
https://streamers.channel/wp-json/wpdd/v1/webhook/{product_id}/{passcode}
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
https://streamers.channel/wp-json/wpdd/v1/webhook/123/a1b2c3d4e5f6
|
||||
```
|
||||
|
||||
Where:
|
||||
- `123` is the WordPress post ID of the software product
|
||||
- `a1b2c3d4e5f6` is the randomly generated passcode for security
|
||||
|
||||
## Testing the Integration
|
||||
|
||||
To test this webhook integration:
|
||||
|
||||
1. Create a software product in WordPress admin
|
||||
2. Set the product type to "Software License"
|
||||
3. Configure the Git repository URL and credentials
|
||||
4. Copy the generated webhook URL from the metabox
|
||||
5. Add the webhook URL to your Gitea repository settings:
|
||||
- Go to Settings → Webhooks
|
||||
- Add new webhook with the WPDD URL
|
||||
- Select "Release events" as the trigger
|
||||
- Set Content-Type to "application/json"
|
||||
6. Create and publish a new release in Gitea
|
||||
7. Check the `wpdd_webhook_events` table to see the received payload
|
||||
8. Check the `wpdd_software_versions` table to see the processed release
|
||||
|
||||
This corrected implementation properly receives release notifications FROM Git platforms like Gitea, rather than attempting to push to them.
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user