2026-01-21 19:15:19 +01:00
|
|
|
/**
|
|
|
|
|
* WC Licensed Product - Version Management
|
|
|
|
|
*
|
|
|
|
|
* @package Jeremias\WcLicensedProduct
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
(function($) {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var WCLicensedProductVersions = {
|
2026-01-21 19:46:50 +01:00
|
|
|
mediaFrame: null,
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
init: function() {
|
|
|
|
|
this.bindEvents();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
bindEvents: function() {
|
|
|
|
|
$('#add-version-btn').on('click', this.addVersion);
|
|
|
|
|
$(document).on('click', '.delete-version-btn', this.deleteVersion);
|
|
|
|
|
$(document).on('click', '.toggle-version-btn', this.toggleVersion);
|
2026-01-21 19:46:50 +01:00
|
|
|
|
|
|
|
|
// Media uploader events
|
|
|
|
|
$('#upload-version-file-btn').on('click', this.openMediaUploader.bind(this));
|
|
|
|
|
$('#remove-version-file-btn').on('click', this.removeSelectedFile);
|
|
|
|
|
|
2026-01-22 17:26:48 +01:00
|
|
|
// Checksum file events
|
|
|
|
|
$('#select-checksum-file-btn').on('click', this.triggerChecksumFileSelect);
|
|
|
|
|
$('#new_checksum_file').on('change', this.onChecksumFileSelected);
|
|
|
|
|
$('#remove-checksum-file-btn').on('click', this.removeChecksumFile);
|
|
|
|
|
|
2026-01-21 19:46:50 +01:00
|
|
|
// Listen for product type changes
|
|
|
|
|
$('#product-type').on('change', this.onProductTypeChange);
|
|
|
|
|
|
|
|
|
|
// Initial check for product type (handles page load for new products)
|
|
|
|
|
this.onProductTypeChange();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onProductTypeChange: function() {
|
|
|
|
|
var productType = $('#product-type').val();
|
|
|
|
|
var $metaBox = $('#wc_licensed_product_versions');
|
|
|
|
|
|
|
|
|
|
if (productType === 'licensed') {
|
|
|
|
|
$metaBox.show();
|
|
|
|
|
} else {
|
|
|
|
|
$metaBox.hide();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Open WordPress media uploader
|
|
|
|
|
*/
|
|
|
|
|
openMediaUploader: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
|
|
// If frame already exists, open it
|
|
|
|
|
if (this.mediaFrame) {
|
|
|
|
|
this.mediaFrame.open();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create media frame
|
|
|
|
|
this.mediaFrame = wp.media({
|
|
|
|
|
title: wcLicensedProductVersions.strings.selectFile,
|
|
|
|
|
button: {
|
|
|
|
|
text: wcLicensedProductVersions.strings.useThisFile
|
|
|
|
|
},
|
|
|
|
|
multiple: false,
|
|
|
|
|
library: {
|
|
|
|
|
type: ['application/zip', 'application/x-zip-compressed', 'application/octet-stream']
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// When file is selected
|
|
|
|
|
this.mediaFrame.on('select', function() {
|
|
|
|
|
var attachment = self.mediaFrame.state().get('selection').first().toJSON();
|
|
|
|
|
|
|
|
|
|
// Set attachment ID
|
|
|
|
|
$('#new_attachment_id').val(attachment.id);
|
|
|
|
|
|
|
|
|
|
// Show filename
|
|
|
|
|
$('#selected_file_name').text(attachment.filename);
|
|
|
|
|
$('#remove-version-file-btn').show();
|
|
|
|
|
|
2026-01-22 16:57:54 +01:00
|
|
|
// Show SHA256 hash field
|
|
|
|
|
$('#sha256-hash-row').show();
|
|
|
|
|
|
2026-01-21 19:46:50 +01:00
|
|
|
// Try to extract version from filename
|
|
|
|
|
var extractedVersion = self.extractVersionFromFilename(attachment.filename);
|
|
|
|
|
if (extractedVersion && !$('#new_version').val().trim()) {
|
|
|
|
|
$('#new_version').val(extractedVersion);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.mediaFrame.open();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove selected file
|
|
|
|
|
*/
|
|
|
|
|
removeSelectedFile: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
$('#new_attachment_id').val('');
|
|
|
|
|
$('#selected_file_name').text('');
|
|
|
|
|
$('#remove-version-file-btn').hide();
|
2026-01-22 16:57:54 +01:00
|
|
|
|
2026-01-22 17:13:27 +01:00
|
|
|
// Hide and clear checksum file field
|
2026-01-22 16:57:54 +01:00
|
|
|
$('#sha256-hash-row').hide();
|
2026-01-22 17:13:27 +01:00
|
|
|
$('#new_checksum_file').val('');
|
2026-01-22 17:26:48 +01:00
|
|
|
$('#selected_checksum_name').text('');
|
|
|
|
|
$('#remove-checksum-file-btn').hide();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger checksum file input click
|
|
|
|
|
*/
|
|
|
|
|
triggerChecksumFileSelect: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
$('#new_checksum_file').trigger('click');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle checksum file selection
|
|
|
|
|
*/
|
|
|
|
|
onChecksumFileSelected: function(e) {
|
|
|
|
|
var file = e.target.files[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
$('#selected_checksum_name').text(file.name);
|
|
|
|
|
$('#remove-checksum-file-btn').show();
|
|
|
|
|
} else {
|
|
|
|
|
$('#selected_checksum_name').text('');
|
|
|
|
|
$('#remove-checksum-file-btn').hide();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove selected checksum file
|
|
|
|
|
*/
|
|
|
|
|
removeChecksumFile: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
$('#new_checksum_file').val('');
|
|
|
|
|
$('#selected_checksum_name').text('');
|
|
|
|
|
$('#remove-checksum-file-btn').hide();
|
2026-01-22 17:13:27 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Read checksum from uploaded file
|
|
|
|
|
* Supports formats: "hash filename" or just "hash"
|
|
|
|
|
*/
|
|
|
|
|
readChecksumFile: function(file) {
|
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
|
if (!file) {
|
|
|
|
|
resolve('');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var reader = new FileReader();
|
|
|
|
|
reader.onload = function(e) {
|
|
|
|
|
var content = e.target.result.trim();
|
|
|
|
|
// Extract hash from content (format: "hash filename" or just "hash")
|
|
|
|
|
var match = content.match(/^([a-fA-F0-9]{64})/);
|
|
|
|
|
if (match) {
|
|
|
|
|
resolve(match[1].toLowerCase());
|
|
|
|
|
} else {
|
|
|
|
|
reject(new Error(wcLicensedProductVersions.strings.invalidChecksumFile || 'Invalid checksum file format'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.onerror = function() {
|
|
|
|
|
reject(new Error(wcLicensedProductVersions.strings.checksumReadError || 'Failed to read checksum file'));
|
|
|
|
|
};
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
});
|
2026-01-21 19:46:50 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract version from filename
|
|
|
|
|
* Supports patterns like: plugin-v1.2.3.zip, plugin-1.2.3.zip, v1.2.3.zip
|
|
|
|
|
*/
|
|
|
|
|
extractVersionFromFilename: function(filename) {
|
|
|
|
|
// Remove extension
|
|
|
|
|
var basename = filename.replace(/\.[^/.]+$/, '');
|
|
|
|
|
|
|
|
|
|
// Try various patterns
|
|
|
|
|
var patterns = [
|
|
|
|
|
/[_-]v?(\d+\.\d+\.\d+)$/i, // ends with -v1.2.3 or -1.2.3
|
|
|
|
|
/[_-]v?(\d+\.\d+\.\d+)[_-]/i, // contains -v1.2.3- or -1.2.3-
|
|
|
|
|
/^v?(\d+\.\d+\.\d+)$/i // just version number
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < patterns.length; i++) {
|
|
|
|
|
var match = basename.match(patterns[i]);
|
|
|
|
|
if (match) {
|
|
|
|
|
return match[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-01-21 19:15:19 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
addVersion: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
2026-01-22 17:13:27 +01:00
|
|
|
var self = WCLicensedProductVersions;
|
2026-01-21 19:15:19 +01:00
|
|
|
var $btn = $(this);
|
|
|
|
|
var $spinner = $btn.siblings('.spinner');
|
|
|
|
|
var productId = $btn.data('product-id');
|
|
|
|
|
var version = $('#new_version').val().trim();
|
|
|
|
|
var releaseNotes = $('#new_release_notes').val().trim();
|
2026-01-21 19:46:50 +01:00
|
|
|
var attachmentId = $('#new_attachment_id').val();
|
2026-01-22 17:13:27 +01:00
|
|
|
var checksumFile = $('#new_checksum_file')[0].files[0];
|
2026-01-21 19:15:19 +01:00
|
|
|
|
|
|
|
|
// Validate version
|
|
|
|
|
if (!version) {
|
|
|
|
|
alert(wcLicensedProductVersions.strings.versionRequired);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
|
|
|
alert(wcLicensedProductVersions.strings.versionInvalid);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$btn.prop('disabled', true);
|
|
|
|
|
$spinner.addClass('is-active');
|
|
|
|
|
|
2026-01-22 17:13:27 +01:00
|
|
|
// Read checksum file if provided, then submit
|
|
|
|
|
self.readChecksumFile(checksumFile).then(function(fileHash) {
|
|
|
|
|
$.ajax({
|
|
|
|
|
url: wcLicensedProductVersions.ajaxUrl,
|
|
|
|
|
type: 'POST',
|
|
|
|
|
data: {
|
|
|
|
|
action: 'wc_licensed_product_add_version',
|
|
|
|
|
nonce: wcLicensedProductVersions.nonce,
|
|
|
|
|
product_id: productId,
|
|
|
|
|
version: version,
|
|
|
|
|
release_notes: releaseNotes,
|
|
|
|
|
attachment_id: attachmentId,
|
|
|
|
|
file_hash: fileHash
|
|
|
|
|
},
|
|
|
|
|
success: function(response) {
|
|
|
|
|
if (response.success) {
|
|
|
|
|
// Remove "no versions" row if present
|
|
|
|
|
$('#versions-table tbody .no-versions').remove();
|
|
|
|
|
|
|
|
|
|
// Add new row to table
|
|
|
|
|
$('#versions-table tbody').prepend(response.data.html);
|
|
|
|
|
|
|
|
|
|
// Clear form
|
|
|
|
|
$('#new_version').val('');
|
|
|
|
|
$('#new_release_notes').val('');
|
|
|
|
|
$('#new_attachment_id').val('');
|
|
|
|
|
$('#selected_file_name').text('');
|
|
|
|
|
$('#remove-version-file-btn').hide();
|
|
|
|
|
$('#sha256-hash-row').hide();
|
|
|
|
|
$('#new_checksum_file').val('');
|
2026-01-22 17:26:48 +01:00
|
|
|
$('#selected_checksum_name').text('');
|
|
|
|
|
$('#remove-checksum-file-btn').hide();
|
2026-01-22 17:13:27 +01:00
|
|
|
} else {
|
|
|
|
|
alert(response.data.message || wcLicensedProductVersions.strings.error);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error: function() {
|
|
|
|
|
alert(wcLicensedProductVersions.strings.error);
|
|
|
|
|
},
|
|
|
|
|
complete: function() {
|
|
|
|
|
$btn.prop('disabled', false);
|
|
|
|
|
$spinner.removeClass('is-active');
|
2026-01-21 19:15:19 +01:00
|
|
|
}
|
2026-01-22 17:13:27 +01:00
|
|
|
});
|
|
|
|
|
}).catch(function(error) {
|
|
|
|
|
alert(error.message);
|
|
|
|
|
$btn.prop('disabled', false);
|
|
|
|
|
$spinner.removeClass('is-active');
|
2026-01-21 19:15:19 +01:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deleteVersion: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
if (!confirm(wcLicensedProductVersions.strings.confirmDelete)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var $btn = $(this);
|
|
|
|
|
var $row = $btn.closest('tr');
|
|
|
|
|
var versionId = $btn.data('version-id');
|
|
|
|
|
|
|
|
|
|
$btn.prop('disabled', true);
|
|
|
|
|
|
|
|
|
|
$.ajax({
|
|
|
|
|
url: wcLicensedProductVersions.ajaxUrl,
|
|
|
|
|
type: 'POST',
|
|
|
|
|
data: {
|
|
|
|
|
action: 'wc_licensed_product_delete_version',
|
|
|
|
|
nonce: wcLicensedProductVersions.nonce,
|
|
|
|
|
version_id: versionId
|
|
|
|
|
},
|
|
|
|
|
success: function(response) {
|
|
|
|
|
if (response.success) {
|
|
|
|
|
$row.fadeOut(300, function() {
|
|
|
|
|
$(this).remove();
|
|
|
|
|
|
|
|
|
|
// Show "no versions" message if table is empty
|
|
|
|
|
if ($('#versions-table tbody tr').length === 0) {
|
|
|
|
|
$('#versions-table tbody').append(
|
|
|
|
|
'<tr class="no-versions"><td colspan="6">' +
|
|
|
|
|
'No versions found. Add your first version above.' +
|
|
|
|
|
'</td></tr>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
alert(response.data.message || wcLicensedProductVersions.strings.error);
|
|
|
|
|
$btn.prop('disabled', false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error: function() {
|
|
|
|
|
alert(wcLicensedProductVersions.strings.error);
|
|
|
|
|
$btn.prop('disabled', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleVersion: function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
var $btn = $(this);
|
|
|
|
|
var $row = $btn.closest('tr');
|
|
|
|
|
var versionId = $btn.data('version-id');
|
|
|
|
|
var currentlyActive = $btn.data('active') === 1 || $btn.data('active') === '1';
|
|
|
|
|
|
|
|
|
|
$btn.prop('disabled', true);
|
|
|
|
|
|
|
|
|
|
$.ajax({
|
|
|
|
|
url: wcLicensedProductVersions.ajaxUrl,
|
|
|
|
|
type: 'POST',
|
|
|
|
|
data: {
|
|
|
|
|
action: 'wc_licensed_product_toggle_version',
|
|
|
|
|
nonce: wcLicensedProductVersions.nonce,
|
|
|
|
|
version_id: versionId,
|
|
|
|
|
currently_active: currentlyActive ? 1 : 0
|
|
|
|
|
},
|
|
|
|
|
success: function(response) {
|
|
|
|
|
if (response.success) {
|
|
|
|
|
var isActive = response.data.isActive;
|
|
|
|
|
var $status = $row.find('.version-status');
|
|
|
|
|
|
|
|
|
|
// Update status badge
|
|
|
|
|
$status
|
|
|
|
|
.removeClass('version-status-active version-status-inactive')
|
|
|
|
|
.addClass('version-status-' + (isActive ? 'active' : 'inactive'))
|
|
|
|
|
.text(isActive ? 'Active' : 'Inactive');
|
|
|
|
|
|
|
|
|
|
// Update button
|
|
|
|
|
$btn
|
|
|
|
|
.data('active', isActive ? 1 : 0)
|
|
|
|
|
.text(isActive ? 'Deactivate' : 'Activate');
|
|
|
|
|
} else {
|
|
|
|
|
alert(response.data.message || wcLicensedProductVersions.strings.error);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
error: function() {
|
|
|
|
|
alert(wcLicensedProductVersions.strings.error);
|
|
|
|
|
},
|
|
|
|
|
complete: function() {
|
|
|
|
|
$btn.prop('disabled', false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$(document).ready(function() {
|
|
|
|
|
WCLicensedProductVersions.init();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
})(jQuery);
|