Files
wc-licensed-product/src/Admin/AdminController.php
magdev 38a9f0d90f Add Test and Transfer actions to PHP fallback template
The PHP fallback template (used when Twig fails) was missing the Test
license action and Transfer modal that were present in the Twig template.

- Added Test license link to row actions in PHP fallback
- Added Transfer link to row actions in PHP fallback
- Added Test License modal with AJAX validation
- Added Transfer License modal
- Added JavaScript handlers for both modals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:41:05 +01:00

1834 lines
86 KiB
PHP

<?php
/**
* Admin Controller
*
* @package Jeremias\WcLicensedProduct\Admin
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Admin;
use Jeremias\WcLicensedProduct\License\License;
use Jeremias\WcLicensedProduct\License\LicenseManager;
use Twig\Environment;
/**
* Handles admin pages for license management
*/
final class AdminController
{
private Environment $twig;
private LicenseManager $licenseManager;
public function __construct(Environment $twig, LicenseManager $licenseManager)
{
$this->twig = $twig;
$this->licenseManager = $licenseManager;
$this->registerHooks();
}
/**
* Register WordPress hooks
*/
private function registerHooks(): void
{
// Add admin menu
add_action('admin_menu', [$this, 'addAdminMenu']);
// Enqueue admin styles
add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']);
// Handle admin actions
add_action('admin_init', [$this, 'handleAdminActions']);
// Add licenses column to orders list
add_filter('manage_edit-shop_order_columns', [$this, 'addOrdersLicenseColumn']);
add_action('manage_shop_order_posts_custom_column', [$this, 'displayOrdersLicenseColumn'], 10, 2);
// HPOS compatibility
add_filter('woocommerce_shop_order_list_table_columns', [$this, 'addOrdersLicenseColumn']);
add_action('woocommerce_shop_order_list_table_custom_column', [$this, 'displayOrdersLicenseColumnHpos'], 10, 2);
// Add to WooCommerce Reports
add_filter('woocommerce_admin_reports', [$this, 'addLicenseReports']);
// AJAX handler for live search
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']);
// AJAX handlers for inline editing
add_action('wp_ajax_wclp_update_license_status', [$this, 'handleAjaxStatusUpdate']);
add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
// AJAX handler for license testing
add_action('wp_ajax_wclp_test_license', [$this, 'handleAjaxTestLicense']);
}
/**
* Add admin menu pages
*/
public function addAdminMenu(): void
{
add_submenu_page(
'woocommerce',
__('Licenses', 'wc-licensed-product'),
__('Licenses', 'wc-licensed-product'),
'manage_woocommerce',
'wc-licenses',
[$this, 'renderLicensesPage']
);
}
/**
* Add license reports to WooCommerce Reports
*/
public function addLicenseReports(array $reports): array
{
$reports['licenses'] = [
'title' => __('Licenses', 'wc-licensed-product'),
'reports' => [
'overview' => [
'title' => __('Overview', 'wc-licensed-product'),
'description' => '',
'hide_title' => true,
'callback' => [$this, 'renderDashboardPage'],
],
],
];
return $reports;
}
/**
* Enqueue admin styles and scripts
*/
public function enqueueStyles(string $hook): void
{
// Check for our pages and WooCommerce Reports page with licenses tab
$isLicensePage = in_array($hook, ['woocommerce_page_wc-licenses', 'woocommerce_page_wc-license-dashboard'], true);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking current page context
$currentTab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : '';
$isReportsPage = $hook === 'woocommerce_page_wc-reports' && $currentTab === 'licenses';
if (!$isLicensePage && !$isReportsPage) {
return;
}
wp_enqueue_style(
'wc-licensed-product-admin',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/admin.css',
[],
WC_LICENSED_PRODUCT_VERSION
);
// Enqueue live search script on licenses page
if ($isLicensePage) {
wp_enqueue_script(
'wc-licensed-product-admin',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/admin-licenses.js',
['jquery'],
WC_LICENSED_PRODUCT_VERSION,
true
);
wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_live_search'),
'editNonce' => wp_create_nonce('wclp_inline_edit'),
'strings' => [
'noResults' => __('No licenses found', 'wc-licensed-product'),
'searching' => __('Searching...', 'wc-licensed-product'),
'error' => __('Search failed', 'wc-licensed-product'),
'saving' => __('Saving...', 'wc-licensed-product'),
'saved' => __('Saved', 'wc-licensed-product'),
'saveFailed' => __('Save failed', 'wc-licensed-product'),
'confirmRevoke' => __('Are you sure you want to revoke this license? This action cannot be undone.', 'wc-licensed-product'),
'edit' => __('Edit', 'wc-licensed-product'),
'cancel' => __('Cancel', 'wc-licensed-product'),
'save' => __('Save', 'wc-licensed-product'),
'lifetime' => __('Lifetime', 'wc-licensed-product'),
'copied' => __('Copied!', 'wc-licensed-product'),
'copyFailed' => __('Copy failed', 'wc-licensed-product'),
],
'statuses' => [
['value' => 'active', 'label' => __('Active', 'wc-licensed-product')],
['value' => 'inactive', 'label' => __('Inactive', 'wc-licensed-product')],
['value' => 'expired', 'label' => __('Expired', 'wc-licensed-product')],
['value' => 'revoked', 'label' => __('Revoked', 'wc-licensed-product')],
],
]);
}
}
/**
* Handle AJAX live search request
*/
public function handleLiveSearch(): void
{
check_ajax_referer('wclp_live_search', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
if (strlen($search) < 2) {
wp_send_json_success(['results' => []]);
}
$filters = ['search' => $search];
$licenses = $this->licenseManager->getAllLicenses(1, 10, $filters);
$results = [];
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$customer = get_userdata($license->getCustomerId());
$results[] = [
'id' => $license->getId(),
'license_key' => $license->getLicenseKey(),
'domain' => $license->getDomain(),
'status' => $license->getStatus(),
'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'),
'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'),
'customer_email' => $customer ? $customer->user_email : '',
'view_url' => admin_url('admin.php?page=wc-licenses&s=' . urlencode($license->getLicenseKey())),
];
}
wp_send_json_success(['results' => $results]);
}
/**
* Handle AJAX status update
*/
public function handleAjaxStatusUpdate(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
$status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
if (!$licenseId) {
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
}
$validStatuses = [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_EXPIRED, License::STATUS_REVOKED];
if (!in_array($status, $validStatuses, true)) {
wp_send_json_error(['message' => __('Invalid status.', 'wc-licensed-product')]);
}
$success = $this->licenseManager->updateLicenseStatus($licenseId, $status);
if ($success) {
wp_send_json_success([
'message' => __('Status updated successfully.', 'wc-licensed-product'),
'status' => $status,
'status_label' => ucfirst($status),
]);
} else {
wp_send_json_error(['message' => __('Failed to update status.', 'wc-licensed-product')]);
}
}
/**
* Handle AJAX expiry date update
*/
public function handleAjaxExpiryUpdate(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
$expiryDate = isset($_POST['expiry_date']) ? sanitize_text_field($_POST['expiry_date']) : '';
if (!$licenseId) {
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
}
// Handle "lifetime" option
if (empty($expiryDate) || strtolower($expiryDate) === 'lifetime') {
$success = $this->licenseManager->setLicenseLifetime($licenseId);
if ($success) {
wp_send_json_success([
'message' => __('License set to lifetime.', 'wc-licensed-product'),
'expiry_date' => '',
'expiry_display' => __('Lifetime', 'wc-licensed-product'),
]);
} else {
wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
}
return;
}
// Validate date format
try {
$date = new \DateTimeImmutable($expiryDate);
$success = $this->licenseManager->updateLicenseExpiry($licenseId, $date);
if ($success) {
wp_send_json_success([
'message' => __('Expiry date updated successfully.', 'wc-licensed-product'),
'expiry_date' => $date->format('Y-m-d'),
'expiry_display' => $date->format(get_option('date_format')),
]);
} else {
wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
}
} catch (\Exception $e) {
wp_send_json_error(['message' => __('Invalid date format.', 'wc-licensed-product')]);
}
}
/**
* Handle AJAX domain update
*/
public function handleAjaxDomainUpdate(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
$domain = isset($_POST['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')]);
}
$success = $this->licenseManager->transferLicense($licenseId, $domain);
if ($success) {
// Get the normalized domain from the license
$license = $this->licenseManager->getLicenseById($licenseId);
$normalizedDomain = $license ? $license->getDomain() : $domain;
wp_send_json_success([
'message' => __('Domain updated successfully.', 'wc-licensed-product'),
'domain' => $normalizedDomain,
]);
} else {
wp_send_json_error(['message' => __('Failed to update domain.', 'wc-licensed-product')]);
}
}
/**
* Handle AJAX revoke
*/
public function handleAjaxRevoke(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
if (!$licenseId) {
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
}
$success = $this->licenseManager->updateLicenseStatus($licenseId, License::STATUS_REVOKED);
if ($success) {
wp_send_json_success([
'message' => __('License revoked successfully.', 'wc-licensed-product'),
'status' => License::STATUS_REVOKED,
'status_label' => ucfirst(License::STATUS_REVOKED),
]);
} else {
wp_send_json_error(['message' => __('Failed to revoke license.', 'wc-licensed-product')]);
}
}
/**
* Handle AJAX license test - validates license against the API
*/
public function handleAjaxTestLicense(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseKey = isset($_POST['license_key']) ? sanitize_text_field(wp_unslash($_POST['license_key'])) : '';
$domain = isset($_POST['domain']) ? sanitize_text_field(wp_unslash($_POST['domain'])) : '';
if (empty($licenseKey) || empty($domain)) {
wp_send_json_error(['message' => __('License key and domain are required.', 'wc-licensed-product')]);
}
// Validate the license using LicenseManager
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
wp_send_json_success($result);
}
/**
* Handle admin actions (update, delete licenses)
*/
public function handleAdminActions(): void
{
if (!isset($_GET['page']) || $_GET['page'] !== 'wc-licenses') {
return;
}
if (!current_user_can('manage_woocommerce')) {
return;
}
// Handle status update
if (isset($_POST['action']) && $_POST['action'] === 'update_license_status') {
$this->handleStatusUpdate();
}
// Handle delete
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['license_id'])) {
$this->handleDelete();
}
// Handle revoke
if (isset($_GET['action']) && $_GET['action'] === 'revoke' && isset($_GET['license_id'])) {
$this->handleRevoke();
}
// Handle extend
if (isset($_GET['action']) && $_GET['action'] === 'extend' && isset($_GET['license_id'])) {
$this->handleExtend();
}
// Handle set lifetime
if (isset($_GET['action']) && $_GET['action'] === 'lifetime' && isset($_GET['license_id'])) {
$this->handleSetLifetime();
}
// Handle bulk actions
if (isset($_POST['bulk_action']) && !empty($_POST['license_ids'])) {
$this->handleBulkAction();
}
// Handle transfer
if (isset($_POST['action']) && $_POST['action'] === 'transfer_license') {
$this->handleTransfer();
}
// Handle CSV export
if (isset($_GET['action']) && $_GET['action'] === 'export_csv') {
$this->handleCsvExport();
}
// Handle CSV import page
if (isset($_GET['action']) && $_GET['action'] === 'import_csv') {
// Show import form - handled in renderImportPage
}
// Handle CSV import upload
if (isset($_POST['action']) && $_POST['action'] === 'process_import_csv') {
$this->handleCsvImport();
}
}
/**
* Handle license status update
*/
private function handleStatusUpdate(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'update_license_status')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_POST['license_id'] ?? 0);
$status = sanitize_text_field($_POST['status'] ?? '');
if ($licenseId && in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_REVOKED], true)) {
$this->licenseManager->updateLicenseStatus($licenseId, $status);
wp_redirect(admin_url('admin.php?page=wc-licenses&updated=1'));
exit;
}
}
/**
* Handle license deletion
*/
private function handleDelete(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'delete_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->deleteLicense($licenseId);
wp_redirect(admin_url('admin.php?page=wc-licenses&deleted=1'));
exit;
}
}
/**
* Handle license revocation
*/
private function handleRevoke(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'revoke_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->updateLicenseStatus($licenseId, License::STATUS_REVOKED);
wp_redirect(admin_url('admin.php?page=wc-licenses&revoked=1'));
exit;
}
}
/**
* Handle license extension
*/
private function handleExtend(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'extend_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
$days = absint($_GET['days'] ?? 30);
if ($licenseId && $days > 0) {
$this->licenseManager->extendLicense($licenseId, $days);
wp_redirect(admin_url('admin.php?page=wc-licenses&extended=1'));
exit;
}
}
/**
* Handle set license to lifetime
*/
private function handleSetLifetime(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'lifetime_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->setLicenseLifetime($licenseId);
wp_redirect(admin_url('admin.php?page=wc-licenses&lifetime=1'));
exit;
}
}
/**
* Handle license transfer
*/
private function handleTransfer(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'transfer_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_POST['license_id'] ?? 0);
$newDomain = sanitize_text_field($_POST['new_domain'] ?? '');
if ($licenseId && !empty($newDomain)) {
$success = $this->licenseManager->transferLicense($licenseId, $newDomain);
if ($success) {
wp_redirect(admin_url('admin.php?page=wc-licenses&transferred=1'));
} else {
wp_redirect(admin_url('admin.php?page=wc-licenses&transfer_failed=1'));
}
exit;
}
wp_redirect(admin_url('admin.php?page=wc-licenses'));
exit;
}
/**
* Handle CSV export
*/
private function handleCsvExport(): void
{
if (!current_user_can('manage_woocommerce')) {
wp_die(__('You do not have permission to export licenses.', 'wc-licensed-product'));
}
$data = $this->licenseManager->exportLicensesForCsv();
if (empty($data)) {
wp_redirect(admin_url('admin.php?page=wc-licenses&export_empty=1'));
exit;
}
// Set headers for CSV download
$filename = 'licenses-export-' . gmdate('Y-m-d-His') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
header('Pragma: no-cache');
header('Expires: 0');
$output = fopen('php://output', 'w');
// Write BOM for UTF-8
fwrite($output, "\xEF\xBB\xBF");
// Write header row
fputcsv($output, array_keys($data[0]));
// Write data rows
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;
}
/**
* Handle CSV import
*/
private function handleCsvImport(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'import_licenses_csv')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
if (!current_user_can('manage_woocommerce')) {
wp_die(__('You do not have permission to import licenses.', 'wc-licensed-product'));
}
// Check if file was uploaded
if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=upload'));
exit;
}
$file = $_FILES['import_file'];
// Validate file type
$fileType = wp_check_filetype($file['name']);
if ($fileType['ext'] !== 'csv') {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=filetype'));
exit;
}
// Read the CSV file
$handle = fopen($file['tmp_name'], 'r');
if (!$handle) {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=read'));
exit;
}
// Get import options
$skipFirstRow = isset($_POST['skip_first_row']) && $_POST['skip_first_row'] === '1';
$updateExisting = isset($_POST['update_existing']) && $_POST['update_existing'] === '1';
// Skip BOM if present
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
// Read header row if skipping
if ($skipFirstRow) {
fgetcsv($handle);
}
$imported = 0;
$updated = 0;
$skipped = 0;
$errors = [];
while (($row = fgetcsv($handle)) !== false) {
// Skip empty rows
if (empty($row) || (count($row) === 1 && empty($row[0]))) {
continue;
}
// Map CSV columns (expected format from export):
// ID, License Key, Product, Product ID, Order ID, Order Number, Customer, Customer Email, Customer ID, Domain, Status, Activations, Max Activations, Expires At, Created At, Updated At
// For import we need: License Key (or generate), Product ID, Customer ID, Domain, Status, Max Activations, Expires At
$result = $this->processImportRow($row, $updateExisting);
if ($result === 'imported') {
$imported++;
} elseif ($result === 'updated') {
$updated++;
} elseif ($result === 'skipped') {
$skipped++;
} else {
$errors[] = $result;
}
}
fclose($handle);
// Build redirect URL with results
$redirectUrl = add_query_arg([
'page' => 'wc-licenses',
'imported' => $imported,
'updated' => $updated,
'skipped' => $skipped,
'import_errors' => count($errors),
], admin_url('admin.php'));
wp_redirect($redirectUrl);
exit;
}
/**
* Process a single import row
*
* @param array $row CSV row data
* @param bool $updateExisting Whether to update existing licenses
* @return string Result: 'imported', 'updated', 'skipped', or error message
*/
private function processImportRow(array $row, bool $updateExisting): string
{
// Determine if this is from our export format or simplified format
// Export format has 16 columns, simplified has fewer
if (count($row) >= 10) {
// Full export format
$licenseKey = trim($row[1] ?? '');
$productId = absint($row[3] ?? 0);
$orderId = absint($row[4] ?? 0);
$customerId = absint($row[8] ?? 0);
$domain = trim($row[9] ?? '');
$status = strtolower(trim($row[10] ?? 'active'));
$activationsCount = absint($row[11] ?? 1);
$maxActivations = absint($row[12] ?? 1);
$expiresAt = trim($row[13] ?? '');
} else {
// Simplified format: License Key, Product ID, Customer ID, Domain, Status, Max Activations, Expires At
$licenseKey = trim($row[0] ?? '');
$productId = absint($row[1] ?? 0);
$customerId = absint($row[2] ?? 0);
$domain = trim($row[3] ?? '');
$status = strtolower(trim($row[4] ?? 'active'));
$maxActivations = absint($row[5] ?? 1);
$expiresAt = trim($row[6] ?? '');
$orderId = 0;
$activationsCount = 1;
}
// Validate required fields
if (empty($domain)) {
return sprintf(__('Row missing domain', 'wc-licensed-product'));
}
if ($productId <= 0) {
return sprintf(__('Row missing valid product ID', 'wc-licensed-product'));
}
// Check if license key already exists
if (!empty($licenseKey)) {
$existing = $this->licenseManager->getLicenseByKey($licenseKey);
if ($existing) {
if ($updateExisting) {
// Update existing license
$this->licenseManager->updateLicenseDomain($existing->getId(), $domain);
if (in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_REVOKED], true)) {
$this->licenseManager->updateLicenseStatus($existing->getId(), $status);
}
return 'updated';
}
return 'skipped';
}
} else {
// Generate new license key
$licenseKey = $this->licenseManager->generateLicenseKey();
while ($this->licenseManager->getLicenseByKey($licenseKey)) {
$licenseKey = $this->licenseManager->generateLicenseKey();
}
}
// Normalize status
if (!in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_EXPIRED, License::STATUS_REVOKED], true)) {
$status = License::STATUS_ACTIVE;
}
// Parse expiration date
$expiresAtParsed = null;
if (!empty($expiresAt) && strtolower($expiresAt) !== 'lifetime') {
try {
$expiresAtParsed = new \DateTimeImmutable($expiresAt);
} catch (\Exception $e) {
// Invalid date, leave as null (lifetime)
}
}
// Create the license
$result = $this->licenseManager->importLicense(
$licenseKey,
$productId,
$customerId,
$domain,
$orderId,
$status,
$maxActivations,
$activationsCount,
$expiresAtParsed
);
return $result ? 'imported' : sprintf(__('Failed to import license for domain %s', 'wc-licensed-product'), $domain);
}
/**
* Handle bulk actions
*/
private function handleBulkAction(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'bulk_license_action')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$action = sanitize_text_field($_POST['bulk_action'] ?? '');
$licenseIds = array_map('absint', (array) ($_POST['license_ids'] ?? []));
if (empty($licenseIds)) {
wp_redirect(admin_url('admin.php?page=wc-licenses'));
exit;
}
$count = 0;
switch ($action) {
case 'activate':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_ACTIVE);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_activated=' . $count));
break;
case 'deactivate':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_INACTIVE);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_deactivated=' . $count));
break;
case 'revoke':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_REVOKED);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_revoked=' . $count));
break;
case 'delete':
$count = $this->licenseManager->bulkDelete($licenseIds);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_deleted=' . $count));
break;
case 'extend_30':
$count = $this->licenseManager->bulkExtend($licenseIds, 30);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
case 'extend_90':
$count = $this->licenseManager->bulkExtend($licenseIds, 90);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
case 'extend_365':
$count = $this->licenseManager->bulkExtend($licenseIds, 365);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
default:
wp_redirect(admin_url('admin.php?page=wc-licenses'));
}
exit;
}
/**
* Render license dashboard page
*/
public function renderDashboardPage(): void
{
$stats = $this->licenseManager->getStatistics();
try {
echo $this->twig->render('admin/dashboard.html.twig', [
'stats' => $stats,
'admin_url' => admin_url('admin.php'),
]);
} catch (\Exception $e) {
// Fallback to PHP template
$this->renderDashboardPageFallback($stats);
}
}
/**
* Fallback render for dashboard page
*/
private function renderDashboardPageFallback(array $stats): void
{
?>
<div class="wrap wclp-dashboard">
<h1><?php esc_html_e('License Dashboard', 'wc-licensed-product'); ?></h1>
<div class="wclp-dashboard-stats">
<div class="wclp-stat-cards">
<div class="wclp-stat-card wclp-stat-total">
<div class="wclp-stat-icon"><span class="dashicons dashicons-admin-network"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['total']); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Total Licenses', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-active">
<div class="wclp-stat-icon"><span class="dashicons dashicons-yes-alt"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_ACTIVE]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Active', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-inactive">
<div class="wclp-stat-icon"><span class="dashicons dashicons-marker"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_INACTIVE]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-expired">
<div class="wclp-stat-icon"><span class="dashicons dashicons-calendar-alt"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_EXPIRED]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Expired', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-revoked">
<div class="wclp-stat-icon"><span class="dashicons dashicons-dismiss"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_REVOKED]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></span>
</div>
</div>
</div>
<?php if ($stats['expiring_soon'] > 0): ?>
<div class="notice notice-warning">
<p>
<span class="dashicons dashicons-warning"></span>
<strong><?php esc_html_e('Attention:', 'wc-licensed-product'); ?></strong>
<?php
printf(
/* translators: %d: number of licenses expiring */
_n(
'%d license is expiring within the next 30 days.',
'%d licenses are expiring within the next 30 days.',
$stats['expiring_soon'],
'wc-licensed-product'
),
$stats['expiring_soon']
);
?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>"><?php esc_html_e('View Licenses', 'wc-licensed-product'); ?></a>
</p>
</div>
<?php endif; ?>
<div class="wclp-dashboard-actions">
<h2><?php esc_html_e('Quick Actions', 'wc-licensed-product'); ?></h2>
<div class="wclp-action-buttons">
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="button button-primary">
<span class="dashicons dashicons-admin-network"></span>
<?php esc_html_e('Manage Licenses', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="button">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e('Export to CSV', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-settings&tab=licensed_product')); ?>" class="button">
<span class="dashicons dashicons-admin-generic"></span>
<?php esc_html_e('Settings', 'wc-licensed-product'); ?>
</a>
</div>
</div>
</div>
</div>
<?php
}
/**
* Render licenses admin page
*/
public function renderLicensesPage(): void
{
// Check if showing import page
if (isset($_GET['action']) && $_GET['action'] === 'import_csv') {
$this->renderImportPage();
return;
}
$page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
$perPage = 20;
// Build filters from query params
$filters = [];
if (!empty($_GET['s'])) {
$filters['search'] = sanitize_text_field($_GET['s']);
}
if (!empty($_GET['status']) && $_GET['status'] !== 'all') {
$filters['status'] = sanitize_text_field($_GET['status']);
}
if (!empty($_GET['product_id'])) {
$filters['product_id'] = absint($_GET['product_id']);
}
$licenses = $this->licenseManager->getAllLicenses($page, $perPage, $filters);
$totalLicenses = $this->licenseManager->getLicenseCount($filters);
$totalPages = (int) ceil($totalLicenses / $perPage);
// Get products for filter dropdown
$licensedProducts = $this->licenseManager->getLicensedProducts();
// Enrich licenses with related data
$enrichedLicenses = [];
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$order = wc_get_order($license->getOrderId());
$customer = get_userdata($license->getCustomerId());
$enrichedLicenses[] = [
'license' => $license,
'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'),
'product_edit_url' => $product ? get_edit_post_link($product->get_id()) : '',
'order_number' => $order ? $order->get_order_number() : '',
'order_edit_url' => $order ? $order->get_edit_order_url() : '',
'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'),
'customer_email' => $customer ? $customer->user_email : '',
];
}
// Add URL helper functions to Twig
$this->twig->addFunction(new \Twig\TwigFunction('extend_url', function (int $licenseId, int $days = 30): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $licenseId . '&days=' . $days),
'extend_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('lifetime_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=lifetime&license_id=' . $licenseId),
'lifetime_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('revoke_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=revoke&license_id=' . $licenseId),
'revoke_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('delete_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=delete&license_id=' . $licenseId),
'delete_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('transfer_nonce', function (): string {
return wp_create_nonce('transfer_license');
}));
try {
echo $this->twig->render('admin/licenses.html.twig', [
'licenses' => $enrichedLicenses,
'current_page' => $page,
'total_pages' => $totalPages,
'total_licenses' => $totalLicenses,
'admin_url' => admin_url('admin.php?page=wc-licenses'),
'notices' => $this->getNotices(),
'filters' => $filters,
'products' => $licensedProducts,
]);
} catch (\Exception $e) {
// Fallback to PHP template
$this->renderLicensesPageFallback($enrichedLicenses, $page, $totalPages, $totalLicenses, $filters, $licensedProducts);
}
}
/**
* Get admin notices
*/
private function getNotices(): array
{
$notices = [];
if (isset($_GET['updated'])) {
$notices[] = ['type' => 'success', 'message' => __('License updated successfully.', 'wc-licensed-product')];
}
if (isset($_GET['deleted'])) {
$notices[] = ['type' => 'success', 'message' => __('License deleted successfully.', 'wc-licensed-product')];
}
if (isset($_GET['revoked'])) {
$notices[] = ['type' => 'success', 'message' => __('License revoked successfully.', 'wc-licensed-product')];
}
if (isset($_GET['extended'])) {
$notices[] = ['type' => 'success', 'message' => __('License extended successfully.', 'wc-licensed-product')];
}
if (isset($_GET['lifetime'])) {
$notices[] = ['type' => 'success', 'message' => __('License set to lifetime successfully.', 'wc-licensed-product')];
}
if (isset($_GET['bulk_activated'])) {
$count = absint($_GET['bulk_activated']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license activated.', '%d licenses activated.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_deactivated'])) {
$count = absint($_GET['bulk_deactivated']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license deactivated.', '%d licenses deactivated.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_revoked'])) {
$count = absint($_GET['bulk_revoked']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license revoked.', '%d licenses revoked.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_deleted'])) {
$count = absint($_GET['bulk_deleted']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license deleted.', '%d licenses deleted.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_extended'])) {
$count = absint($_GET['bulk_extended']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license extended.', '%d licenses extended.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['transferred'])) {
$notices[] = ['type' => 'success', 'message' => __('License transferred to new domain successfully.', 'wc-licensed-product')];
}
if (isset($_GET['transfer_failed'])) {
$notices[] = ['type' => 'error', 'message' => __('Failed to transfer license. The license may be revoked or invalid.', 'wc-licensed-product')];
}
if (isset($_GET['export_empty'])) {
$notices[] = ['type' => 'warning', 'message' => __('No licenses to export.', 'wc-licensed-product')];
}
if (isset($_GET['imported'])) {
$imported = absint($_GET['imported']);
$updated = absint($_GET['updated'] ?? 0);
$skipped = absint($_GET['skipped'] ?? 0);
$errors = absint($_GET['import_errors'] ?? 0);
$message = sprintf(
/* translators: %d: number of licenses imported */
_n('%d license imported.', '%d licenses imported.', $imported, 'wc-licensed-product'),
$imported
);
if ($updated > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of licenses updated */
_n('%d updated.', '%d updated.', $updated, 'wc-licensed-product'),
$updated
);
}
if ($skipped > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of licenses skipped */
_n('%d skipped.', '%d skipped.', $skipped, 'wc-licensed-product'),
$skipped
);
}
if ($errors > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of errors */
_n('%d error.', '%d errors.', $errors, 'wc-licensed-product'),
$errors
);
}
$notices[] = ['type' => 'success', 'message' => $message];
}
return $notices;
}
/**
* Fallback render for licenses page
*/
private function renderLicensesPageFallback(array $enrichedLicenses, int $page, int $totalPages, int $totalLicenses, array $filters = [], array $products = []): void
{
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h1>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="page-title-action">
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
<?php esc_html_e('Export CSV', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=import_csv')); ?>" class="page-title-action">
<span class="dashicons dashicons-upload" style="vertical-align: middle;"></span>
<?php esc_html_e('Import CSV', 'wc-licensed-product'); ?>
</a>
<hr class="wp-header-end">
<?php foreach ($this->getNotices() as $notice): ?>
<div class="notice notice-<?php echo esc_attr($notice['type']); ?> is-dismissible">
<p><?php echo esc_html($notice['message']); ?></p>
</div>
<?php endforeach; ?>
<!-- Search and Filter Form -->
<form method="get" action="" class="wclp-filter-form">
<input type="hidden" name="page" value="wc-licenses">
<p class="search-box">
<label class="screen-reader-text" for="license-search-input"><?php esc_html_e('Search Licenses', 'wc-licensed-product'); ?></label>
<input type="search" id="license-search-input" name="s" value="<?php echo esc_attr($filters['search'] ?? ''); ?>"
placeholder="<?php esc_attr_e('Search license key or domain...', 'wc-licensed-product'); ?>">
<input type="submit" id="search-submit" class="button" value="<?php esc_attr_e('Search', 'wc-licensed-product'); ?>">
</p>
<div class="tablenav top">
<div class="alignleft actions">
<select name="status">
<option value="all"><?php esc_html_e('All Statuses', 'wc-licensed-product'); ?></option>
<option value="active" <?php selected($filters['status'] ?? '', 'active'); ?>><?php esc_html_e('Active', 'wc-licensed-product'); ?></option>
<option value="inactive" <?php selected($filters['status'] ?? '', 'inactive'); ?>><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></option>
<option value="expired" <?php selected($filters['status'] ?? '', 'expired'); ?>><?php esc_html_e('Expired', 'wc-licensed-product'); ?></option>
<option value="revoked" <?php selected($filters['status'] ?? '', 'revoked'); ?>><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></option>
</select>
<select name="product_id">
<option value=""><?php esc_html_e('All Products', 'wc-licensed-product'); ?></option>
<?php foreach ($products as $id => $name): ?>
<option value="<?php echo esc_attr($id); ?>" <?php selected($filters['product_id'] ?? '', $id); ?>><?php echo esc_html($name); ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="<?php esc_attr_e('Filter', 'wc-licensed-product'); ?>">
<?php if (!empty($filters)): ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="button"><?php esc_html_e('Clear', 'wc-licensed-product'); ?></a>
<?php endif; ?>
</div>
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html($totalLicenses); ?> <?php echo $totalLicenses === 1 ? esc_html__('item', 'wc-licensed-product') : esc_html__('items', 'wc-licensed-product'); ?></span>
</div>
</div>
</form>
<p class="description">
<?php esc_html_e('Showing', 'wc-licensed-product'); ?> <?php echo esc_html($totalLicenses); ?> <?php echo $totalLicenses === 1 ? esc_html__('license', 'wc-licensed-product') : esc_html__('licenses', 'wc-licensed-product'); ?>
<?php if (!empty($filters)): ?>
(<?php esc_html_e('filtered', 'wc-licensed-product'); ?>)
<?php endif; ?>
| <a href="<?php echo esc_url(admin_url('admin.php?page=wc-reports&tab=licenses')); ?>"><?php esc_html_e('View Dashboard', 'wc-licensed-product'); ?></a>
</p>
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
<?php wp_nonce_field('bulk_license_action'); ?>
<div class="tablenav top">
<div class="alignleft actions bulkactions">
<select name="bulk_action" id="bulk-action-selector">
<option value=""><?php esc_html_e('Bulk Actions', 'wc-licensed-product'); ?></option>
<option value="activate"><?php esc_html_e('Activate', 'wc-licensed-product'); ?></option>
<option value="deactivate"><?php esc_html_e('Deactivate', 'wc-licensed-product'); ?></option>
<option value="revoke"><?php esc_html_e('Revoke', 'wc-licensed-product'); ?></option>
<option value="extend_30"><?php esc_html_e('Extend 30 days', 'wc-licensed-product'); ?></option>
<option value="extend_90"><?php esc_html_e('Extend 90 days', 'wc-licensed-product'); ?></option>
<option value="extend_365"><?php esc_html_e('Extend 1 year', 'wc-licensed-product'); ?></option>
<option value="delete"><?php esc_html_e('Delete', 'wc-licensed-product'); ?></option>
</select>
<input type="submit" class="button action" value="<?php esc_attr_e('Apply', 'wc-licensed-product'); ?>">
</div>
</div>
<table class="wp-list-table widefat fixed striped licenses-table">
<thead>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="cb-select-all-1">
</td>
<th><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Created', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($enrichedLicenses)): ?>
<tr>
<td colspan="9"><?php esc_html_e('No licenses found.', 'wc-licensed-product'); ?></td>
</tr>
<?php else: ?>
<?php foreach ($enrichedLicenses as $item): ?>
<tr>
<th scope="row" class="check-column">
<input type="checkbox" name="license_ids[]" value="<?php echo esc_attr($item['license']->getId()); ?>">
</th>
<td>
<code class="wclp-license-key"><?php echo esc_html($item['license']->getLicenseKey()); ?></code>
<button type="button" class="wclp-copy-btn button-link" data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-clipboard"></span>
</button>
</td>
<td>
<?php if ($item['product_edit_url']): ?>
<a href="<?php echo esc_url($item['product_edit_url']); ?>">
<?php echo esc_html($item['product_name']); ?>
</a>
<?php else: ?>
<?php echo esc_html($item['product_name']); ?>
<?php endif; ?>
</td>
<td>
<?php echo esc_html($item['customer_name']); ?>
<?php if ($item['customer_email']): ?>
<br><small><?php echo esc_html($item['customer_email']); ?></small>
<?php endif; ?>
</td>
<td class="wclp-editable-cell" data-field="domain" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<span class="wclp-display-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="text" class="wclp-edit-input" value="<?php echo esc_attr($item['license']->getDomain()); ?>">
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</div>
</td>
<td class="wclp-editable-cell" data-field="status" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<span class="wclp-display-value">
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
</span>
</span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<select class="wclp-edit-input">
<option value="active" <?php selected($item['license']->getStatus(), 'active'); ?>><?php esc_html_e('Active', 'wc-licensed-product'); ?></option>
<option value="inactive" <?php selected($item['license']->getStatus(), 'inactive'); ?>><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></option>
<option value="expired" <?php selected($item['license']->getStatus(), 'expired'); ?>><?php esc_html_e('Expired', 'wc-licensed-product'); ?></option>
<option value="revoked" <?php selected($item['license']->getStatus(), 'revoked'); ?>><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></option>
</select>
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</div>
</td>
<td class="wclp-created-cell">
<?php echo esc_html($item['license']->getCreatedAt()->format(get_option('date_format'))); ?>
</td>
<td class="wclp-editable-cell" data-field="expiry" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
<span class="wclp-display-value">
<?php if ($expiresAt): ?>
<?php echo esc_html($expiresAt->format(get_option('date_format'))); ?>
<?php else: ?>
<span class="license-lifetime"><?php esc_html_e('Lifetime', 'wc-licensed-product'); ?></span>
<?php endif; ?>
</span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="date" class="wclp-edit-input" value="<?php echo $expiresAt ? esc_attr($expiresAt->format('Y-m-d')) : ''; ?>" placeholder="<?php esc_attr_e('Leave empty for lifetime', 'wc-licensed-product'); ?>">
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-lifetime-btn button button-small" title="<?php esc_attr_e('Set to lifetime', 'wc-licensed-product'); ?>">∞</button>
</div>
</td>
<td class="license-actions">
<div class="row-actions">
<span class="test">
<a href="#" class="wclp-test-license-link"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>"
data-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
title="<?php esc_attr_e('Test license against API', 'wc-licensed-product'); ?>"><?php esc_html_e('Test', 'wc-licensed-product'); ?></a> |
</span>
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
<span class="transfer">
<a href="#" class="wclp-transfer-link"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
data-current-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>"><?php esc_html_e('Transfer', 'wc-licensed-product'); ?></a> |
</span>
<span class="extend">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $item['license']->getId() . '&days=30'),
'extend_license'
)); ?>" title="<?php esc_attr_e('Extend by 30 days', 'wc-licensed-product'); ?>">+30d</a> |
</span>
<span class="lifetime">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=lifetime&license_id=' . $item['license']->getId()),
'lifetime_license'
)); ?>" title="<?php esc_attr_e('Set to lifetime', 'wc-licensed-product'); ?>">∞</a> |
</span>
<span class="revoke">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=revoke&license_id=' . $item['license']->getId()),
'revoke_license'
)); ?>" onclick="return confirm('<?php esc_attr_e('Are you sure?', 'wc-licensed-product'); ?>')">
<?php esc_html_e('Revoke', 'wc-licensed-product'); ?>
</a> |
</span>
<?php endif; ?>
<span class="delete">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=delete&license_id=' . $item['license']->getId()),
'delete_license'
)); ?>" class="submitdelete" onclick="return confirm('<?php esc_attr_e('Are you sure you want to delete this license?', 'wc-licensed-product'); ?>')">
<?php esc_html_e('Delete', 'wc-licensed-product'); ?>
</a>
</span>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tfoot>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="cb-select-all-2">
</td>
<th><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Created', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
</tr>
</tfoot>
</table>
<div class="tablenav bottom">
<div class="alignleft actions bulkactions">
<select name="bulk_action_2" id="bulk-action-selector-bottom">
<option value=""><?php esc_html_e('Bulk Actions', 'wc-licensed-product'); ?></option>
<option value="activate"><?php esc_html_e('Activate', 'wc-licensed-product'); ?></option>
<option value="deactivate"><?php esc_html_e('Deactivate', 'wc-licensed-product'); ?></option>
<option value="revoke"><?php esc_html_e('Revoke', 'wc-licensed-product'); ?></option>
<option value="extend_30"><?php esc_html_e('Extend 30 days', 'wc-licensed-product'); ?></option>
<option value="extend_90"><?php esc_html_e('Extend 90 days', 'wc-licensed-product'); ?></option>
<option value="extend_365"><?php esc_html_e('Extend 1 year', 'wc-licensed-product'); ?></option>
<option value="delete"><?php esc_html_e('Delete', 'wc-licensed-product'); ?></option>
</select>
<input type="submit" class="button action" value="<?php esc_attr_e('Apply', 'wc-licensed-product'); ?>">
</div>
<?php if ($totalPages > 1): ?>
<div class="tablenav-pages">
<?php
echo paginate_links([
'base' => admin_url('admin.php?page=wc-licenses&paged=%#%'),
'format' => '',
'current' => $page,
'total' => $totalPages,
]);
?>
</div>
<?php endif; ?>
</div>
</form>
<!-- Test License Modal -->
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2><?php esc_html_e('License Validation Test', 'wc-licensed-product'); ?></h2>
<div class="wclp-test-info">
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<td><code id="test-license-key"></code></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<td><code id="test-domain"></code></td>
</tr>
</table>
</div>
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
<span class="spinner is-active" style="float:none;"></span>
<p><?php esc_html_e('Testing license...', 'wc-licensed-product'); ?></p>
</div>
<div id="wclp-test-result" style="display:none;">
<div id="wclp-test-result-content"></div>
</div>
<p class="submit">
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Close', 'wc-licensed-product'); ?></button>
</p>
</div>
</div>
<!-- Transfer Modal -->
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2><?php esc_html_e('Transfer License to New Domain', 'wc-licensed-product'); ?></h2>
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
<input type="hidden" name="action" value="transfer_license">
<?php wp_nonce_field('transfer_license', '_wpnonce'); ?>
<input type="hidden" name="license_id" id="transfer-license-id" value="">
<table class="form-table">
<tr>
<th scope="row"><label><?php esc_html_e('Current Domain', 'wc-licensed-product'); ?></label></th>
<td><code id="transfer-current-domain"></code></td>
</tr>
<tr>
<th scope="row"><label for="new_domain"><?php esc_html_e('New Domain', 'wc-licensed-product'); ?></label></th>
<td>
<input type="text" name="new_domain" id="transfer-new-domain" class="regular-text" placeholder="example.com" required>
<p class="description"><?php esc_html_e('Enter the new domain without http:// or www.', 'wc-licensed-product'); ?></p>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Transfer License', 'wc-licensed-product'); ?></button>
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</p>
</form>
</div>
</div>
<script>
(function($) {
// Checkbox select all
$('#cb-select-all-1, #cb-select-all-2').on('change', function() {
$('input[name="license_ids[]"]').prop('checked', this.checked);
$('#cb-select-all-1, #cb-select-all-2').prop('checked', this.checked);
});
$('#bulk-action-selector, #bulk-action-selector-bottom').on('change', function() {
$('#bulk-action-selector, #bulk-action-selector-bottom').val($(this).val());
});
$('form').on('submit', function() {
var topAction = $('#bulk-action-selector').val();
var bottomAction = $('#bulk-action-selector-bottom').val();
if (!topAction && bottomAction) {
$('#bulk-action-selector').val(bottomAction);
}
});
// Transfer modal
var $transferModal = $('#wclp-transfer-modal');
$('.wclp-transfer-link').on('click', function(e) {
e.preventDefault();
var licenseId = $(this).data('license-id');
var currentDomain = $(this).data('current-domain');
$('#transfer-license-id').val(licenseId);
$('#transfer-current-domain').text(currentDomain);
$('#transfer-new-domain').val('');
$transferModal.show();
});
// Test License modal
var $testModal = $('#wclp-test-modal');
var $testLoading = $('#wclp-test-loading');
var $testResult = $('#wclp-test-result');
var $testResultContent = $('#wclp-test-result-content');
$('.wclp-test-license-link').on('click', function(e) {
e.preventDefault();
var licenseKey = $(this).data('license-key');
var domain = $(this).data('domain');
$('#test-license-key').text(licenseKey);
$('#test-domain').text(domain);
$testLoading.show();
$testResult.hide();
$testModal.show();
$.ajax({
url: wclpAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wclp_test_license',
nonce: wclpAdmin.editNonce,
license_key: licenseKey,
domain: domain
},
success: function(response) {
$testLoading.hide();
if (response.success) {
var result = response.data;
var html = '';
if (result.valid) {
html = '<div class="notice notice-success inline"><p><strong>✓ <?php echo esc_js(__('License is VALID', 'wc-licensed-product')); ?></strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th><?php echo esc_js(__('Product', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
html += '<tr><th><?php echo esc_js(__('Version', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
if (result.expires_at) {
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
} else {
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td><?php echo esc_js(__('Lifetime', 'wc-licensed-product')); ?></td></tr>';
}
html += '</tbody></table>';
} else {
html = '<div class="notice notice-error inline"><p><strong>✗ <?php echo esc_js(__('License is INVALID', 'wc-licensed-product')); ?></strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th><?php echo esc_js(__('Error Code', 'wc-licensed-product')); ?></th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
html += '<tr><th><?php echo esc_js(__('Message', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
html += '</tbody></table>';
}
$testResultContent.html(html);
$testResult.show();
} else {
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
$testResult.show();
}
},
error: function() {
$testLoading.hide();
$testResultContent.html('<div class="notice notice-error inline"><p><?php echo esc_js(__('Failed to test license. Please try again.', 'wc-licensed-product')); ?></p></div>');
$testResult.show();
}
});
});
// Close modals
$('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
$(this).closest('.wclp-modal').hide();
});
$(window).on('click', function(e) {
if ($(e.target).hasClass('wclp-modal')) {
$(e.target).hide();
}
});
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})(jQuery);
</script>
</div>
<?php
}
/**
* Render the CSV import page
*/
private function renderImportPage(): void
{
$importError = $_GET['import_error'] ?? '';
?>
<div class="wrap">
<h1>
<?php esc_html_e('Import Licenses', 'wc-licensed-product'); ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="page-title-action">
<?php esc_html_e('Back to Licenses', 'wc-licensed-product'); ?>
</a>
</h1>
<?php if ($importError): ?>
<div class="notice notice-error">
<p>
<?php
switch ($importError) {
case 'upload':
esc_html_e('Error uploading file. Please try again.', 'wc-licensed-product');
break;
case 'filetype':
esc_html_e('Invalid file type. Please upload a CSV file.', 'wc-licensed-product');
break;
case 'read':
esc_html_e('Error reading file. Please check the file format.', 'wc-licensed-product');
break;
default:
esc_html_e('An error occurred during import.', 'wc-licensed-product');
}
?>
</p>
</div>
<?php endif; ?>
<div class="card" style="max-width: 800px; padding: 20px;">
<h2><?php esc_html_e('Import Licenses from CSV', 'wc-licensed-product'); ?></h2>
<p class="description">
<?php esc_html_e('Upload a CSV file to import licenses. You can use the exported CSV format or a simplified format.', 'wc-licensed-product'); ?>
</p>
<h3><?php esc_html_e('CSV Format', 'wc-licensed-product'); ?></h3>
<p class="description">
<?php esc_html_e('The CSV file should contain the following columns:', 'wc-licensed-product'); ?>
</p>
<div style="background: #f8f9fa; padding: 15px; border-radius: 4px; margin: 15px 0;">
<p><strong><?php esc_html_e('Full Format (from Export):', 'wc-licensed-product'); ?></strong></p>
<code style="display: block; margin-bottom: 10px; font-size: 12px;">ID, License Key, Product, Product ID, Order ID, Order Number, Customer, Customer Email, Customer ID, Domain, Status, Activations, Max Activations, Expires At, Created At, Updated At</code>
<p><strong><?php esc_html_e('Simplified Format:', 'wc-licensed-product'); ?></strong></p>
<code style="display: block; font-size: 12px;">License Key, Product ID, Customer ID, Domain, Status, Max Activations, Expires At</code>
</div>
<p class="description">
<strong><?php esc_html_e('Notes:', 'wc-licensed-product'); ?></strong><br>
- <?php esc_html_e('Leave License Key empty to auto-generate.', 'wc-licensed-product'); ?><br>
- <?php esc_html_e('Status can be: active, inactive, expired, revoked (defaults to active).', 'wc-licensed-product'); ?><br>
- <?php esc_html_e('Expires At should be in YYYY-MM-DD format or "Lifetime".', 'wc-licensed-product'); ?>
</p>
<hr style="margin: 20px 0;">
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" enctype="multipart/form-data">
<?php wp_nonce_field('import_licenses_csv'); ?>
<input type="hidden" name="action" value="process_import_csv">
<table class="form-table">
<tr>
<th scope="row">
<label for="import_file"><?php esc_html_e('CSV File', 'wc-licensed-product'); ?></label>
</th>
<td>
<input type="file" name="import_file" id="import_file" accept=".csv" required>
<p class="description"><?php esc_html_e('Select a CSV file to import.', 'wc-licensed-product'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('Options', 'wc-licensed-product'); ?></th>
<td>
<label>
<input type="checkbox" name="skip_first_row" value="1" checked>
<?php esc_html_e('Skip first row (header row)', 'wc-licensed-product'); ?>
</label>
<br><br>
<label>
<input type="checkbox" name="update_existing" value="1">
<?php esc_html_e('Update existing licenses (by license key)', 'wc-licensed-product'); ?>
</label>
<p class="description">
<?php esc_html_e('If enabled, licenses with matching keys will be updated instead of skipped.', 'wc-licensed-product'); ?>
</p>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">
<span class="dashicons dashicons-upload" style="vertical-align: middle;"></span>
<?php esc_html_e('Import Licenses', 'wc-licensed-product'); ?>
</button>
</p>
</form>
</div>
</div>
<?php
}
/**
* Add license column to orders list
*/
public function addOrdersLicenseColumn(array $columns): array
{
$newColumns = [];
foreach ($columns as $key => $value) {
$newColumns[$key] = $value;
if ($key === 'order_status') {
$newColumns['license'] = __('License', 'wc-licensed-product');
}
}
return $newColumns;
}
/**
* Display license column content
*/
public function displayOrdersLicenseColumn(string $column, int $postId): void
{
if ($column !== 'license') {
return;
}
$order = wc_get_order($postId);
$this->outputLicenseColumnContent($order);
}
/**
* Display license column content (HPOS)
*/
public function displayOrdersLicenseColumnHpos(string $column, \WC_Order $order): void
{
if ($column !== 'license') {
return;
}
$this->outputLicenseColumnContent($order);
}
/**
* Output license column content
*/
private function outputLicenseColumnContent(?\WC_Order $order): void
{
if (!$order) {
echo '—';
return;
}
$hasLicensedProduct = false;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$hasLicensedProduct = true;
break;
}
}
if (!$hasLicensedProduct) {
echo '—';
return;
}
$domain = $order->get_meta('_licensed_product_domain');
if ($domain) {
echo '<span class="dashicons dashicons-admin-network"></span> ' . esc_html($domain);
} else {
echo '<span class="dashicons dashicons-warning" title="' . esc_attr__('No domain specified', 'wc-licensed-product') . '"></span>';
}
}
}