You've already forked wc-licensed-product
The toggle version button in the admin product versions table was not deactivating versions due to incorrect parameter order in the updateVersion() call. The isActive value was being passed to the attachmentId parameter position instead. - Fixed parameter order: updateVersion($id, null, !$active, null) - Bumped version to 0.3.3 - Updated CHANGELOG.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
430 lines
20 KiB
PHP
430 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Version Admin Controller
|
|
*
|
|
* @package Jeremias\WcLicensedProduct\Admin
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Jeremias\WcLicensedProduct\Admin;
|
|
|
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
|
|
|
/**
|
|
* Handles admin UI for product version management
|
|
*/
|
|
final class VersionAdminController
|
|
{
|
|
private VersionManager $versionManager;
|
|
|
|
public function __construct(VersionManager $versionManager)
|
|
{
|
|
$this->versionManager = $versionManager;
|
|
$this->registerHooks();
|
|
}
|
|
|
|
/**
|
|
* Register WordPress hooks
|
|
*/
|
|
private function registerHooks(): void
|
|
{
|
|
// Add versions meta box to licensed products
|
|
add_action('add_meta_boxes', [$this, 'addVersionsMetaBox']);
|
|
|
|
// Handle AJAX actions for version management
|
|
add_action('wp_ajax_wc_licensed_product_add_version', [$this, 'ajaxAddVersion']);
|
|
add_action('wp_ajax_wc_licensed_product_delete_version', [$this, 'ajaxDeleteVersion']);
|
|
add_action('wp_ajax_wc_licensed_product_toggle_version', [$this, 'ajaxToggleVersion']);
|
|
|
|
// Enqueue scripts for product edit page
|
|
add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
|
|
}
|
|
|
|
/**
|
|
* Add versions meta box to product edit page
|
|
*/
|
|
public function addVersionsMetaBox(): void
|
|
{
|
|
global $post;
|
|
|
|
// Only add meta box for licensed products or new products
|
|
if ($post && $post->post_type === 'product') {
|
|
$product = wc_get_product($post->ID);
|
|
// Show for licensed products or new products (where type might be selected later)
|
|
if (!$product || $product->is_type('licensed') || $post->post_status === 'auto-draft') {
|
|
add_meta_box(
|
|
'wc_licensed_product_versions',
|
|
__('Product Versions', 'wc-licensed-product'),
|
|
[$this, 'renderVersionsMetaBox'],
|
|
'product',
|
|
'normal',
|
|
'high'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render versions meta box
|
|
*/
|
|
public function renderVersionsMetaBox(\WP_Post $post): void
|
|
{
|
|
$versions = $this->versionManager->getVersionsByProduct($post->ID);
|
|
wp_nonce_field('wc_licensed_product_versions', 'wc_licensed_product_versions_nonce');
|
|
?>
|
|
<div class="wc-licensed-product-versions">
|
|
<div class="versions-add-form">
|
|
<h4><?php esc_html_e('Add New Version', 'wc-licensed-product'); ?></h4>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="new_version"><?php esc_html_e('Version', 'wc-licensed-product'); ?></label></th>
|
|
<td>
|
|
<input type="text" id="new_version" name="new_version" placeholder="1.0.0" class="regular-text" pattern="[0-9]+\.[0-9]+\.[0-9]+" />
|
|
<p class="description"><?php esc_html_e('Use semantic versioning (e.g., 1.0.0)', 'wc-licensed-product'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="new_attachment_id"><?php esc_html_e('Download File', 'wc-licensed-product'); ?></label></th>
|
|
<td>
|
|
<input type="hidden" id="new_attachment_id" name="new_attachment_id" value="" />
|
|
<span id="selected_file_name" class="selected-file-name"></span>
|
|
<button type="button" class="button" id="upload-version-file-btn">
|
|
<?php esc_html_e('Select File', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<button type="button" class="button" id="remove-version-file-btn" style="display: none;">
|
|
<?php esc_html_e('Remove', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<p class="description"><?php esc_html_e('Upload or select a file from the media library. Version will be auto-detected from filename (e.g., plugin-v1.2.3.zip).', 'wc-licensed-product'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr id="sha256-hash-row" style="display: none;">
|
|
<th><label for="new_checksum_file"><?php esc_html_e('Checksum File', 'wc-licensed-product'); ?></label></th>
|
|
<td>
|
|
<input type="file" id="new_checksum_file" name="new_checksum_file" accept=".sha256,.txt" style="display: none;" />
|
|
<span id="selected_checksum_name" class="selected-file-name"></span>
|
|
<button type="button" class="button" id="select-checksum-file-btn">
|
|
<?php esc_html_e('Select Checksum File', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<button type="button" class="button" id="remove-checksum-file-btn" style="display: none;">
|
|
<?php esc_html_e('Remove', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<p class="description"><?php esc_html_e('Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity.', 'wc-licensed-product'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="new_release_notes"><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></label></th>
|
|
<td>
|
|
<textarea id="new_release_notes" name="new_release_notes" rows="3" class="large-text"></textarea>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p>
|
|
<button type="button" class="button button-primary" id="add-version-btn" data-product-id="<?php echo esc_attr($post->ID); ?>">
|
|
<?php esc_html_e('Add Version', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<span class="spinner" style="float: none;"></span>
|
|
</p>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<h4><?php esc_html_e('Existing Versions', 'wc-licensed-product'); ?></h4>
|
|
<table class="wp-list-table widefat fixed striped" id="versions-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('SHA256', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th>
|
|
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($versions)): ?>
|
|
<tr class="no-versions">
|
|
<td colspan="7"><?php esc_html_e('No versions found. Add your first version above.', 'wc-licensed-product'); ?></td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($versions as $version): ?>
|
|
<tr data-version-id="<?php echo esc_attr($version->getId()); ?>">
|
|
<td><strong><?php echo esc_html($version->getVersion()); ?></strong></td>
|
|
<td>
|
|
<?php
|
|
$effectiveUrl = $version->getEffectiveDownloadUrl();
|
|
$filename = $version->getDownloadFilename();
|
|
if ($effectiveUrl):
|
|
?>
|
|
<span class="version-download-link">
|
|
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
|
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
|
</a>
|
|
<?php if ($version->getAttachmentId()): ?>
|
|
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($version->getFileHash()): ?>
|
|
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
|
|
<?php else: ?>
|
|
<em>—</em>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
|
<td>
|
|
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
|
<?php echo $version->isActive() ? esc_html__('Active', 'wc-licensed-product') : esc_html__('Inactive', 'wc-licensed-product'); ?>
|
|
</span>
|
|
</td>
|
|
<td><?php echo esc_html($version->getReleasedAt()->format(get_option('date_format'))); ?></td>
|
|
<td>
|
|
<button type="button" class="button button-small toggle-version-btn" data-version-id="<?php echo esc_attr($version->getId()); ?>" data-active="<?php echo $version->isActive() ? '1' : '0'; ?>">
|
|
<?php echo $version->isActive() ? esc_html__('Deactivate', 'wc-licensed-product') : esc_html__('Activate', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<button type="button" class="button button-small button-link-delete delete-version-btn" data-version-id="<?php echo esc_attr($version->getId()); ?>">
|
|
<?php esc_html_e('Delete', 'wc-licensed-product'); ?>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Enqueue scripts for product edit page
|
|
*/
|
|
public function enqueueScripts(string $hook): void
|
|
{
|
|
if ($hook !== 'post.php' && $hook !== 'post-new.php') {
|
|
return;
|
|
}
|
|
|
|
global $post;
|
|
if (!$post || $post->post_type !== 'product') {
|
|
return;
|
|
}
|
|
|
|
// Enqueue WordPress media uploader
|
|
wp_enqueue_media();
|
|
|
|
wp_enqueue_script(
|
|
'wc-licensed-product-versions',
|
|
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/versions.js',
|
|
['jquery'],
|
|
WC_LICENSED_PRODUCT_VERSION,
|
|
true
|
|
);
|
|
|
|
wp_localize_script('wc-licensed-product-versions', 'wcLicensedProductVersions', [
|
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('wc_licensed_product_versions'),
|
|
'strings' => [
|
|
'confirmDelete' => __('Are you sure you want to delete this version?', 'wc-licensed-product'),
|
|
'versionRequired' => __('Please enter a version number.', 'wc-licensed-product'),
|
|
'versionInvalid' => __('Please enter a valid version number (e.g., 1.0.0).', 'wc-licensed-product'),
|
|
'error' => __('An error occurred. Please try again.', 'wc-licensed-product'),
|
|
'selectFile' => __('Select Download File', 'wc-licensed-product'),
|
|
'useThisFile' => __('Use this file', 'wc-licensed-product'),
|
|
'invalidChecksumFile' => __('Invalid checksum file format. File must contain a 64-character SHA256 hash.', 'wc-licensed-product'),
|
|
'checksumReadError' => __('Failed to read checksum file.', 'wc-licensed-product'),
|
|
],
|
|
]);
|
|
|
|
wp_enqueue_style(
|
|
'wc-licensed-product-admin',
|
|
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/admin.css',
|
|
[],
|
|
WC_LICENSED_PRODUCT_VERSION
|
|
);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for adding a version
|
|
*/
|
|
public function ajaxAddVersion(): void
|
|
{
|
|
check_ajax_referer('wc_licensed_product_versions', 'nonce');
|
|
|
|
if (!current_user_can('manage_woocommerce')) {
|
|
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
$productId = absint($_POST['product_id'] ?? 0);
|
|
$version = sanitize_text_field($_POST['version'] ?? '');
|
|
$releaseNotes = sanitize_textarea_field($_POST['release_notes'] ?? '');
|
|
$attachmentId = absint($_POST['attachment_id'] ?? 0);
|
|
$fileHash = sanitize_text_field($_POST['file_hash'] ?? '');
|
|
|
|
if (!$productId || !$version) {
|
|
wp_send_json_error(['message' => __('Product ID and version are required.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
// Validate version format
|
|
if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
|
|
wp_send_json_error(['message' => __('Invalid version format. Use semantic versioning (e.g., 1.0.0).', 'wc-licensed-product')]);
|
|
}
|
|
|
|
// Check if version already exists
|
|
if ($this->versionManager->versionExists($productId, $version)) {
|
|
wp_send_json_error(['message' => __('This version already exists.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
// Verify product exists and is of type licensed
|
|
$product = wc_get_product($productId);
|
|
if (!$product) {
|
|
wp_send_json_error(['message' => __('Product not found.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
if (!$product->is_type('licensed')) {
|
|
wp_send_json_error(['message' => __('This product is not a licensed product.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
try {
|
|
$newVersion = $this->versionManager->createVersion(
|
|
$productId,
|
|
$version,
|
|
$releaseNotes ?: null,
|
|
$attachmentId ?: null,
|
|
$fileHash ?: null
|
|
);
|
|
} catch (\InvalidArgumentException $e) {
|
|
wp_send_json_error(['message' => $e->getMessage()]);
|
|
}
|
|
|
|
if (!$newVersion) {
|
|
global $wpdb;
|
|
$errorMessage = __('Failed to create version.', 'wc-licensed-product');
|
|
if (!empty($wpdb->last_error)) {
|
|
error_log('WC Licensed Product: DB error - ' . $wpdb->last_error);
|
|
}
|
|
wp_send_json_error(['message' => $errorMessage]);
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => __('Version added successfully.', 'wc-licensed-product'),
|
|
'version' => $newVersion->toArray(),
|
|
'html' => $this->getVersionRowHtml($newVersion),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting a version
|
|
*/
|
|
public function ajaxDeleteVersion(): void
|
|
{
|
|
check_ajax_referer('wc_licensed_product_versions', 'nonce');
|
|
|
|
if (!current_user_can('manage_woocommerce')) {
|
|
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
$versionId = absint($_POST['version_id'] ?? 0);
|
|
|
|
if (!$versionId) {
|
|
wp_send_json_error(['message' => __('Version ID is required.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
$result = $this->versionManager->deleteVersion($versionId);
|
|
|
|
if (!$result) {
|
|
wp_send_json_error(['message' => __('Failed to delete version.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
wp_send_json_success(['message' => __('Version deleted successfully.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for toggling version status
|
|
*/
|
|
public function ajaxToggleVersion(): void
|
|
{
|
|
check_ajax_referer('wc_licensed_product_versions', 'nonce');
|
|
|
|
if (!current_user_can('manage_woocommerce')) {
|
|
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
$versionId = absint($_POST['version_id'] ?? 0);
|
|
$currentlyActive = (bool) ($_POST['currently_active'] ?? false);
|
|
|
|
if (!$versionId) {
|
|
wp_send_json_error(['message' => __('Version ID is required.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
$result = $this->versionManager->updateVersion($versionId, null, !$currentlyActive, null);
|
|
|
|
if (!$result) {
|
|
wp_send_json_error(['message' => __('Failed to update version.', 'wc-licensed-product')]);
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => __('Version updated successfully.', 'wc-licensed-product'),
|
|
'isActive' => !$currentlyActive,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get HTML for a version table row
|
|
*/
|
|
private function getVersionRowHtml(\Jeremias\WcLicensedProduct\Product\ProductVersion $version): string
|
|
{
|
|
ob_start();
|
|
?>
|
|
<tr data-version-id="<?php echo esc_attr($version->getId()); ?>">
|
|
<td><strong><?php echo esc_html($version->getVersion()); ?></strong></td>
|
|
<td>
|
|
<?php
|
|
$effectiveUrl = $version->getEffectiveDownloadUrl();
|
|
$filename = $version->getDownloadFilename();
|
|
if ($effectiveUrl):
|
|
?>
|
|
<span class="version-download-link">
|
|
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
|
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
|
</a>
|
|
<?php if ($version->getAttachmentId()): ?>
|
|
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($version->getFileHash()): ?>
|
|
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
|
|
<?php else: ?>
|
|
<em>—</em>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
|
<td>
|
|
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
|
<?php echo $version->isActive() ? esc_html__('Active', 'wc-licensed-product') : esc_html__('Inactive', 'wc-licensed-product'); ?>
|
|
</span>
|
|
</td>
|
|
<td><?php echo esc_html($version->getReleasedAt()->format(get_option('date_format'))); ?></td>
|
|
<td>
|
|
<button type="button" class="button button-small toggle-version-btn" data-version-id="<?php echo esc_attr($version->getId()); ?>" data-active="<?php echo $version->isActive() ? '1' : '0'; ?>">
|
|
<?php echo $version->isActive() ? esc_html__('Deactivate', 'wc-licensed-product') : esc_html__('Activate', 'wc-licensed-product'); ?>
|
|
</button>
|
|
<button type="button" class="button button-small button-link-delete delete-version-btn" data-version-id="<?php echo esc_attr($version->getId()); ?>">
|
|
<?php esc_html_e('Delete', 'wc-licensed-product'); ?>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
}
|