Files
wc-licensed-product/src/Admin/VersionAdminController.php
magdev 78e43b9aea Implement version 0.0.3 features
- Add file attachment support for product versions (Media Library)
- Add version auto-detection from uploaded filenames
- Implement secure customer downloads with hash verification
- Add license key copy-to-clipboard functionality
- Redesign customer licenses page with card-based UI
- Fix product versions meta box visibility for non-licensed types
- Add DownloadController for secure file delivery
- Update CLAUDE.md roadmap and session history

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 19:46:50 +01:00

377 lines
17 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
{
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>
<th><label for="new_download_url"><?php esc_html_e('Or External URL', 'wc-licensed-product'); ?></label></th>
<td>
<input type="url" id="new_download_url" name="new_download_url" class="large-text" placeholder="https://" />
<p class="description"><?php esc_html_e('Alternative: Enter an external download URL instead of uploading a file.', '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('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="6"><?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):
?>
<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; ?>
<?php else: ?>
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></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'),
],
]);
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'] ?? '');
$downloadUrl = esc_url_raw($_POST['download_url'] ?? '');
$releaseNotes = sanitize_textarea_field($_POST['release_notes'] ?? '');
$attachmentId = absint($_POST['attachment_id'] ?? 0);
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')]);
}
$newVersion = $this->versionManager->createVersion(
$productId,
$version,
$releaseNotes ?: null,
$downloadUrl ?: null,
$attachmentId ?: null
);
if (!$newVersion) {
wp_send_json_error(['message' => __('Failed to create version.', 'wc-licensed-product')]);
}
// Also update the product's current version meta
update_post_meta($productId, '_licensed_current_version', $version);
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, null, !$currentlyActive);
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):
?>
<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; ?>
<?php else: ?>
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></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();
}
}