You've already forked wc-licensed-product
Wrap filename link and media-archive icon in a flex container with white-space: nowrap to keep them on a single line. 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, 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):
|
|
?>
|
|
<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();
|
|
}
|
|
}
|