Files
wc-licensed-product/src/Admin/VersionAdminController.php
magdev 1bc643408e Fix version deactivation button not working (v0.3.3)
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>
2026-01-22 19:39:26 +01:00

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();
}
}