Files
wc-licensed-product/src/Admin/VersionAdminController.php

430 lines
20 KiB
PHP
Raw Normal View History

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