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 { ?>

0): ?>

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 { ?>


getNotices() as $notice): ?>

() |

getLicenseKey()); ?>
getDomain()); ?> getStatus())); ?> getCreatedAt()->format(get_option('date_format'))); ?> getExpiresAt(); ?> format(get_option('date_format'))); ?>
getStatus() !== License::STATUS_REVOKED): ?> +30d | | |
1): ?>
admin_url('admin.php?page=wc-licenses&paged=%#%'), 'format' => '', 'current' => $page, 'total' => $totalPages, ]); ?>

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

License Key, Product ID, Customer ID, Domain, Status, Max Activations, Expires At


-
-
-




$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 ' ' . esc_html($domain); } else { echo ''; } } }