2026-01-21 19:15:19 +01:00
< ? 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
{
2026-01-21 22:02:51 +01:00
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'
);
}
}
2026-01-21 19:15:19 +01:00
}
/**
* 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 >
2026-01-21 19:46:50 +01:00
< 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 >
2026-01-22 16:57:54 +01:00
< tr id = " sha256-hash-row " style = " display: none; " >
2026-01-22 17:13:27 +01:00
< th >< label for = " new_checksum_file " >< ? php esc_html_e ( 'Checksum File' , 'wc-licensed-product' ); ?> </label></th>
2026-01-21 19:15:19 +01:00
< td >
2026-01-22 17:26:48 +01:00
< 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 >
2026-01-22 17:13:27 +01:00
< p class = " description " >< ? php esc_html_e ( 'Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity.' , 'wc-licensed-product' ); ?> </p>
2026-01-21 19:15:19 +01:00
</ 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>
2026-01-21 19:46:50 +01:00
< th >< ? php esc_html_e ( 'Download File' , 'wc-licensed-product' ); ?> </th>
2026-01-22 17:35:25 +01:00
< th >< ? php esc_html_e ( 'SHA256' , 'wc-licensed-product' ); ?> </th>
2026-01-21 19:15:19 +01:00
< 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 " >
2026-01-22 17:35:25 +01:00
< td colspan = " 7 " >< ? php esc_html_e ( 'No versions found. Add your first version above.' , 'wc-licensed-product' ); ?> </td>
2026-01-21 19:15:19 +01:00
</ 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 >
2026-01-21 19:46:50 +01:00
< ? 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 )); ?>
2026-01-21 19:15:19 +01:00
</ a >
2026-01-21 19:46:50 +01:00
< ? php if ( $version -> getAttachmentId ()) : ?>
< span class = " dashicons dashicons-media-archive " title = " <?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?> " ></ span >
< ? php endif ; ?>
2026-01-21 19:15:19 +01:00
< ? php else : ?>
2026-01-21 19:46:50 +01:00
< em >< ? php esc_html_e ( 'No download file' , 'wc-licensed-product' ); ?> </em>
2026-01-21 19:15:19 +01:00
< ? php endif ; ?>
</ td >
2026-01-22 17:35:25 +01:00
< 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 >
2026-01-21 19:15:19 +01:00
< 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 ;
}
2026-01-21 19:46:50 +01:00
// Enqueue WordPress media uploader
wp_enqueue_media ();
2026-01-21 19:15:19 +01:00
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' ),
2026-01-21 19:46:50 +01:00
'selectFile' => __ ( 'Select Download File' , 'wc-licensed-product' ),
'useThisFile' => __ ( 'Use this file' , 'wc-licensed-product' ),
2026-01-22 17:13:27 +01:00
'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' ),
2026-01-21 19:15:19 +01:00
],
]);
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' ] ? ? '' );
2026-01-21 19:46:50 +01:00
$attachmentId = absint ( $_POST [ 'attachment_id' ] ? ? 0 );
2026-01-22 16:57:54 +01:00
$fileHash = sanitize_text_field ( $_POST [ 'file_hash' ] ? ? '' );
2026-01-21 19:15:19 +01:00
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' )]);
}
2026-01-21 22:02:51 +01:00
// 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' )]);
}
2026-01-22 16:57:54 +01:00
try {
$newVersion = $this -> versionManager -> createVersion (
$productId ,
$version ,
$releaseNotes ? : null ,
$attachmentId ? : null ,
$fileHash ? : null
);
} catch ( \InvalidArgumentException $e ) {
wp_send_json_error ([ 'message' => $e -> getMessage ()]);
}
2026-01-21 19:15:19 +01:00
if ( ! $newVersion ) {
2026-01-21 22:02:51 +01:00
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 ]);
2026-01-21 19:15:19 +01:00
}
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 >
2026-01-21 19:46:50 +01:00
< ? 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 )); ?>
2026-01-21 19:15:19 +01:00
</ a >
2026-01-21 19:46:50 +01:00
< ? php if ( $version -> getAttachmentId ()) : ?>
< span class = " dashicons dashicons-media-archive " title = " <?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?> " ></ span >
< ? php endif ; ?>
2026-01-21 19:15:19 +01:00
< ? php else : ?>
2026-01-21 19:46:50 +01:00
< em >< ? php esc_html_e ( 'No download file' , 'wc-licensed-product' ); ?> </em>
2026-01-21 19:15:19 +01:00
< ? php endif ; ?>
</ td >
2026-01-22 17:35:25 +01:00
< 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 >
2026-01-21 19:15:19 +01:00
< 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 ();
}
}