licenseManager = $licenseManager;
$this->registerHooks();
}
/**
* Register WordPress hooks
*/
private function registerHooks(): void
{
// Add licenses meta box to order edit page
add_action('add_meta_boxes', [$this, 'addLicensesMetaBox']);
// Handle AJAX actions
add_action('wp_ajax_wclp_update_order_domain', [$this, 'ajaxUpdateOrderDomain']);
add_action('wp_ajax_wclp_update_license_domain', [$this, 'ajaxUpdateLicenseDomain']);
add_action('wp_ajax_wclp_generate_order_licenses', [$this, 'ajaxGenerateOrderLicenses']);
// Enqueue admin scripts
add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
}
/**
* Add licenses meta box to order edit page
*/
public function addLicensesMetaBox(): void
{
// Support both classic post type and HPOS
$screen = wc_get_container()->get(\Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class)->custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id('shop-order')
: 'shop_order';
add_meta_box(
'wclp_order_licenses',
__('Product Licenses', 'wc-licensed-product'),
[$this, 'renderLicensesMetaBox'],
$screen,
'normal',
'default'
);
}
/**
* Render licenses meta box
*/
public function renderLicensesMetaBox($post_or_order): void
{
// Get order object - support both classic and HPOS
if ($post_or_order instanceof \WC_Order) {
$order = $post_or_order;
} else {
$order = wc_get_order($post_or_order->ID);
}
if (!$order) {
echo '
' . esc_html__('Order not found.', 'wc-licensed-product') . '
';
return;
}
// Check if order has licensed products
$hasLicensedProduct = false;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $this->licenseManager->isLicensedProduct($product)) {
$hasLicensedProduct = true;
break;
}
}
if (!$hasLicensedProduct) {
echo '' . esc_html__('This order does not contain licensed products.', 'wc-licensed-product') . '
';
return;
}
// Check for multi-domain format first, then fall back to legacy single domain
$multiDomainData = $order->get_meta('_licensed_product_domains');
$legacyDomain = $order->get_meta('_licensed_product_domain');
$hasMultiDomain = !empty($multiDomainData) && is_array($multiDomainData);
// Get licenses for this order
$licenses = $this->licenseManager->getLicensesByOrder($order->get_id());
wp_nonce_field('wclp_order_license_actions', 'wclp_order_license_nonce');
?>
get_name() : __('Unknown Product', 'wc-licensed-product');
?>
:
get_items() as $item) {
$product = $item->get_product();
if ($product && $this->licenseManager->isLicensedProduct($product)) {
$expectedLicenses++;
}
}
}
$missingLicenses = $expectedLicenses - count($licenses);
$hasDomainData = $hasMultiDomain || !empty($legacyDomain);
?>
is_paid()): ?>
is_paid()): ?>
' . esc_html__('Licenses', 'wc-licensed-product') . ''
);
?>
0 && $hasDomainData && $order->is_paid()): ?>
id, ['shop_order', 'woocommerce_page_wc-orders'], true)
|| (isset($_GET['page']) && $_GET['page'] === 'wc-orders' && isset($_GET['action']) && $_GET['action'] === 'edit');
if (!$isOrderEdit) {
return;
}
wp_enqueue_script(
'wclp-order-licenses',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/order-licenses.js',
['jquery'],
WC_LICENSED_PRODUCT_VERSION,
true
);
wp_localize_script('wclp-order-licenses', 'wclpOrderLicenses', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_order_license_actions'),
'strings' => [
'saving' => __('Saving...', 'wc-licensed-product'),
'saved' => __('Saved!', 'wc-licensed-product'),
'error' => __('Error. Please try again.', 'wc-licensed-product'),
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
'generating' => __('Generating...', 'wc-licensed-product'),
],
]);
}
/**
* AJAX handler for updating order domain
*/
public function ajaxUpdateOrderDomain(): void
{
check_ajax_referer('wclp_order_license_actions', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
}
$orderId = absint($_POST['order_id'] ?? 0);
$domain = sanitize_text_field($_POST['domain'] ?? '');
if (!$orderId) {
wp_send_json_error(['message' => __('Invalid order ID.', 'wc-licensed-product')]);
}
$order = wc_get_order($orderId);
if (!$order) {
wp_send_json_error(['message' => __('Order not found.', 'wc-licensed-product')]);
}
// Normalize and validate domain
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (!empty($domain) && !$this->isValidDomain($normalizedDomain)) {
wp_send_json_error(['message' => __('Invalid domain format.', 'wc-licensed-product')]);
}
// Update order meta
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
$order->save();
wp_send_json_success([
'message' => __('Order domain updated.', 'wc-licensed-product'),
'domain' => $normalizedDomain,
]);
}
/**
* AJAX handler for updating license domain
*/
public function ajaxUpdateLicenseDomain(): void
{
check_ajax_referer('wclp_order_license_actions', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
}
$licenseId = absint($_POST['license_id'] ?? 0);
$domain = sanitize_text_field($_POST['domain'] ?? '');
if (!$licenseId) {
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
}
if (empty($domain)) {
wp_send_json_error(['message' => __('Domain cannot be empty.', 'wc-licensed-product')]);
}
// Normalize and validate domain
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (!$this->isValidDomain($normalizedDomain)) {
wp_send_json_error(['message' => __('Invalid domain format.', 'wc-licensed-product')]);
}
// Get license to verify it exists
$license = $this->licenseManager->getLicenseById($licenseId);
if (!$license) {
wp_send_json_error(['message' => __('License not found.', 'wc-licensed-product')]);
}
// Update license domain
$success = $this->licenseManager->updateLicenseDomain($licenseId, $normalizedDomain);
if ($success) {
wp_send_json_success([
'message' => __('License domain updated.', 'wc-licensed-product'),
'domain' => $normalizedDomain,
]);
} else {
wp_send_json_error(['message' => __('Failed to update license domain.', 'wc-licensed-product')]);
}
}
/**
* Validate domain format
*/
private function isValidDomain(string $domain): bool
{
if (empty($domain)) {
return true; // Empty is allowed for order domain
}
if (strlen($domain) > 255) {
return false;
}
$pattern = '/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
return (bool) preg_match($pattern, $domain);
}
/**
* AJAX handler for generating order licenses
*/
public function ajaxGenerateOrderLicenses(): void
{
check_ajax_referer('wclp_order_license_actions', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
}
$orderId = absint($_POST['order_id'] ?? 0);
if (!$orderId) {
wp_send_json_error(['message' => __('Invalid order ID.', 'wc-licensed-product')]);
}
$order = wc_get_order($orderId);
if (!$order) {
wp_send_json_error(['message' => __('Order not found.', 'wc-licensed-product')]);
}
// Check if order is paid
if (!$order->is_paid()) {
wp_send_json_error(['message' => __('Order must be paid before licenses can be generated.', 'wc-licensed-product')]);
}
// Check for multi-domain format first
$multiDomainData = $order->get_meta('_licensed_product_domains');
$legacyDomain = $order->get_meta('_licensed_product_domain');
if (!empty($multiDomainData) && is_array($multiDomainData)) {
// Multi-domain format
$result = $this->generateMultiDomainLicenses($order, $multiDomainData);
} elseif (!empty($legacyDomain)) {
// Legacy single domain format
$result = $this->generateLegacyLicenses($order, $legacyDomain);
} else {
wp_send_json_error(['message' => __('Please set the order domain before generating licenses.', 'wc-licensed-product')]);
return;
}
if ($result['generated'] > 0) {
wp_send_json_success([
'message' => sprintf(
/* translators: %d: Number of licenses generated */
_n(
'%d license generated successfully.',
'%d licenses generated successfully.',
$result['generated'],
'wc-licensed-product'
),
$result['generated']
),
'generated' => $result['generated'],
'skipped' => $result['skipped'],
'reload' => true,
]);
} else {
wp_send_json_success([
'message' => __('All licenses already exist for this order.', 'wc-licensed-product'),
'generated' => 0,
'skipped' => $result['skipped'],
'reload' => false,
]);
}
}
/**
* Generate licenses for multi-domain format
*/
private function generateMultiDomainLicenses(\WC_Order $order, array $domainData): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
// Index domains by product ID
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
}
}
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue;
}
$productId = $product->get_id();
$domains = $domainsByProduct[$productId] ?? [];
// Get existing licenses for this product
$existingLicenses = $this->licenseManager->getLicensesByOrderAndProduct($orderId, $productId);
$existingDomains = array_map(fn($l) => $l->getDomain(), $existingLicenses);
foreach ($domains as $domain) {
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
// Skip if license already exists for this domain
if (in_array($normalizedDomain, $existingDomains, true)) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$productId,
$customerId,
$normalizedDomain
);
if ($license) {
$generated++;
}
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
/**
* Generate licenses for legacy single domain format
*/
private function generateLegacyLicenses(\WC_Order $order, string $domain): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue;
}
// Check if license already exists
$existing = $this->licenseManager->getLicenseByOrderAndProduct($orderId, $product->get_id());
if ($existing) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$product->get_id(),
$customerId,
$domain
);
if ($license) {
$generated++;
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
}