2026-01-21 18:55:18 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Main Plugin class
|
|
|
|
|
*
|
|
|
|
|
* @package Jeremias\WcLicensedProduct
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Jeremias\WcLicensedProduct;
|
|
|
|
|
|
|
|
|
|
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
2026-01-23 16:05:52 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Admin\DashboardWidgetController;
|
2026-01-24 10:17:46 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Admin\DownloadWidgetController;
|
2026-01-21 22:18:27 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
2026-01-21 20:32:35 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
2026-01-21 19:15:19 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
2026-01-22 16:57:54 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
2026-01-21 21:58:54 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
2026-01-21 21:58:54 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Checkout\StoreApiExtension;
|
2026-01-21 19:15:19 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Email\LicenseEmailController;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Frontend\AccountController;
|
2026-01-21 19:46:50 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Frontend\DownloadController;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
2026-01-22 18:32:17 +01:00
|
|
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
2026-01-21 19:15:19 +01:00
|
|
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
2026-01-21 18:55:18 +01:00
|
|
|
use Twig\Environment;
|
|
|
|
|
use Twig\Loader\FilesystemLoader;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Main plugin controller
|
|
|
|
|
*/
|
|
|
|
|
final class Plugin
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Singleton instance
|
|
|
|
|
*/
|
|
|
|
|
private static ?Plugin $instance = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Twig environment
|
|
|
|
|
*/
|
|
|
|
|
private Environment $twig;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* License manager
|
|
|
|
|
*/
|
|
|
|
|
private LicenseManager $licenseManager;
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
/**
|
|
|
|
|
* Version manager
|
|
|
|
|
*/
|
|
|
|
|
private VersionManager $versionManager;
|
|
|
|
|
|
2026-01-21 19:46:50 +01:00
|
|
|
/**
|
|
|
|
|
* Download controller
|
|
|
|
|
*/
|
|
|
|
|
private DownloadController $downloadController;
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* Get singleton instance
|
|
|
|
|
*/
|
|
|
|
|
public static function instance(): Plugin
|
|
|
|
|
{
|
|
|
|
|
if (self::$instance === null) {
|
|
|
|
|
self::$instance = new self();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::$instance;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 23:50:57 +01:00
|
|
|
/**
|
|
|
|
|
* Get singleton instance (alias for instance())
|
|
|
|
|
*/
|
|
|
|
|
public static function getInstance(): Plugin
|
|
|
|
|
{
|
|
|
|
|
return self::instance();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* Private constructor for singleton
|
|
|
|
|
*/
|
|
|
|
|
private function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->initTwig();
|
|
|
|
|
$this->initComponents();
|
|
|
|
|
$this->registerHooks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize Twig environment
|
|
|
|
|
*/
|
|
|
|
|
private function initTwig(): void
|
|
|
|
|
{
|
|
|
|
|
$loader = new FilesystemLoader(WC_LICENSED_PRODUCT_PLUGIN_DIR . 'templates');
|
|
|
|
|
$this->twig = new Environment($loader, [
|
|
|
|
|
'cache' => WP_CONTENT_DIR . '/cache/wc-licensed-product/twig',
|
2026-01-21 22:59:53 +01:00
|
|
|
'auto_reload' => true, // Always check for template changes
|
2026-01-23 21:18:32 +01:00
|
|
|
'autoescape' => 'html', // Explicitly enable HTML autoescape for XSS protection
|
2026-01-21 18:55:18 +01:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Add WordPress functions as Twig functions
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('__', function (string $text, string $domain = 'wc-licensed-product'): string {
|
|
|
|
|
return __($text, $domain);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr'));
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('wp_nonce_field', 'wp_nonce_field', ['is_safe' => ['html']]));
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('admin_url', 'admin_url'));
|
|
|
|
|
$this->twig->addFunction(new \Twig\TwigFunction('wc_get_endpoint_url', 'wc_get_endpoint_url'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize plugin components
|
|
|
|
|
*/
|
|
|
|
|
private function initComponents(): void
|
|
|
|
|
{
|
|
|
|
|
$this->licenseManager = new LicenseManager();
|
2026-01-21 19:15:19 +01:00
|
|
|
$this->versionManager = new VersionManager();
|
2026-01-21 18:55:18 +01:00
|
|
|
|
2026-01-22 18:32:17 +01:00
|
|
|
// Check plugin license
|
|
|
|
|
$licenseChecker = PluginLicenseChecker::getInstance();
|
|
|
|
|
$isLicensed = $licenseChecker->isLicenseValid();
|
|
|
|
|
|
|
|
|
|
// Always initialize product type (needed for existing orders)
|
2026-01-21 18:55:18 +01:00
|
|
|
new LicensedProductType();
|
2026-01-22 18:32:17 +01:00
|
|
|
|
|
|
|
|
// Only initialize frontend components if licensed or on localhost
|
|
|
|
|
if ($isLicensed) {
|
|
|
|
|
new CheckoutController($this->licenseManager);
|
|
|
|
|
new StoreApiExtension($this->licenseManager);
|
|
|
|
|
$this->registerCheckoutBlocksIntegration();
|
|
|
|
|
$this->downloadController = new DownloadController($this->licenseManager, $this->versionManager);
|
|
|
|
|
new AccountController($this->twig, $this->licenseManager, $this->versionManager, $this->downloadController);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always initialize REST API and email controller
|
2026-01-21 18:55:18 +01:00
|
|
|
new RestApiController($this->licenseManager);
|
2026-01-21 19:15:19 +01:00
|
|
|
new LicenseEmailController($this->licenseManager);
|
2026-01-21 18:55:18 +01:00
|
|
|
|
2026-01-22 16:57:54 +01:00
|
|
|
// Initialize response signing if server secret is configured
|
|
|
|
|
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') {
|
|
|
|
|
(new ResponseSigner())->register();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 18:32:17 +01:00
|
|
|
// Admin always available
|
2026-01-21 18:55:18 +01:00
|
|
|
if (is_admin()) {
|
|
|
|
|
new AdminController($this->twig, $this->licenseManager);
|
2026-01-21 19:15:19 +01:00
|
|
|
new VersionAdminController($this->versionManager);
|
2026-01-21 22:18:27 +01:00
|
|
|
new OrderLicenseController($this->licenseManager);
|
2026-01-21 20:32:35 +01:00
|
|
|
new SettingsController();
|
2026-01-23 16:05:52 +01:00
|
|
|
new DashboardWidgetController($this->licenseManager);
|
2026-01-24 10:17:46 +01:00
|
|
|
new DownloadWidgetController($this->versionManager);
|
2026-01-22 18:32:17 +01:00
|
|
|
|
|
|
|
|
// Show admin notice if unlicensed and not on localhost
|
|
|
|
|
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
|
|
|
|
|
add_action('admin_notices', [$this, 'showUnlicensedNotice']);
|
|
|
|
|
}
|
2026-01-21 18:55:18 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 21:58:54 +01:00
|
|
|
/**
|
|
|
|
|
* Register WooCommerce Checkout Blocks integration
|
|
|
|
|
*/
|
|
|
|
|
private function registerCheckoutBlocksIntegration(): void
|
|
|
|
|
{
|
|
|
|
|
add_action('woocommerce_blocks_loaded', function (): void {
|
|
|
|
|
if (class_exists('Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry')) {
|
|
|
|
|
add_action(
|
|
|
|
|
'woocommerce_blocks_checkout_block_registration',
|
|
|
|
|
function ($integration_registry): void {
|
|
|
|
|
$integration_registry->register(new CheckoutBlocksIntegration());
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* Register plugin hooks
|
|
|
|
|
*/
|
|
|
|
|
private function registerHooks(): void
|
|
|
|
|
{
|
2026-01-22 18:32:17 +01:00
|
|
|
// Only register order hooks if licensed (license generation requires valid license)
|
|
|
|
|
$licenseChecker = PluginLicenseChecker::getInstance();
|
|
|
|
|
if ($licenseChecker->isLicenseValid()) {
|
|
|
|
|
// Generate license on order completion (multiple hooks for compatibility)
|
|
|
|
|
add_action('woocommerce_order_status_completed', [$this, 'onOrderCompleted']);
|
|
|
|
|
add_action('woocommerce_order_status_processing', [$this, 'onOrderCompleted']);
|
2026-01-21 21:58:54 +01:00
|
|
|
|
2026-01-22 18:32:17 +01:00
|
|
|
// Also hook into payment complete for immediate license generation
|
|
|
|
|
add_action('woocommerce_payment_complete', [$this, 'onOrderCompleted']);
|
|
|
|
|
}
|
2026-01-21 18:55:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle order completion - generate licenses
|
|
|
|
|
*/
|
|
|
|
|
public function onOrderCompleted(int $orderId): void
|
|
|
|
|
{
|
|
|
|
|
$order = wc_get_order($orderId);
|
|
|
|
|
if (!$order) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 18:31:36 +01:00
|
|
|
// Try new multi-domain format first
|
|
|
|
|
$domainData = $order->get_meta('_licensed_product_domains');
|
|
|
|
|
if (!empty($domainData) && is_array($domainData)) {
|
|
|
|
|
$this->generateLicensesMultiDomain($order, $domainData);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to legacy single domain format
|
|
|
|
|
$this->generateLicensesSingleDomain($order);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate licenses for new multi-domain format
|
|
|
|
|
*/
|
|
|
|
|
private function generateLicensesMultiDomain(\WC_Order $order, array $domainData): void
|
|
|
|
|
{
|
|
|
|
|
$orderId = $order->get_id();
|
|
|
|
|
$customerId = $order->get_customer_id();
|
|
|
|
|
|
|
|
|
|
// Index domains by product ID for quick lookup
|
|
|
|
|
$domainsByProduct = [];
|
|
|
|
|
foreach ($domainData as $item) {
|
|
|
|
|
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
|
|
|
|
|
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate licenses for each licensed product
|
2026-01-21 18:55:18 +01:00
|
|
|
foreach ($order->get_items() as $item) {
|
|
|
|
|
$product = $item->get_product();
|
2026-01-25 18:31:36 +01:00
|
|
|
if (!$product || !$product->is_type('licensed')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$productId = $product->get_id();
|
|
|
|
|
$domains = $domainsByProduct[$productId] ?? [];
|
|
|
|
|
|
|
|
|
|
// Generate a license for each domain
|
|
|
|
|
foreach ($domains as $domain) {
|
|
|
|
|
if (!empty($domain)) {
|
2026-01-21 18:55:18 +01:00
|
|
|
$this->licenseManager->generateLicense(
|
|
|
|
|
$orderId,
|
2026-01-25 18:31:36 +01:00
|
|
|
$productId,
|
|
|
|
|
$customerId,
|
2026-01-21 18:55:18 +01:00
|
|
|
$domain
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 18:31:36 +01:00
|
|
|
/**
|
|
|
|
|
* Generate licenses for legacy single domain format
|
|
|
|
|
*/
|
|
|
|
|
private function generateLicensesSingleDomain(\WC_Order $order): void
|
|
|
|
|
{
|
|
|
|
|
$domain = $order->get_meta('_licensed_product_domain');
|
|
|
|
|
if (empty($domain)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($order->get_items() as $item) {
|
|
|
|
|
$product = $item->get_product();
|
|
|
|
|
if ($product && $product->is_type('licensed')) {
|
|
|
|
|
$this->licenseManager->generateLicense(
|
|
|
|
|
$order->get_id(),
|
|
|
|
|
$product->get_id(),
|
|
|
|
|
$order->get_customer_id(),
|
|
|
|
|
$domain
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 18:55:18 +01:00
|
|
|
/**
|
|
|
|
|
* Get Twig environment
|
|
|
|
|
*/
|
|
|
|
|
public function getTwig(): Environment
|
|
|
|
|
{
|
|
|
|
|
return $this->twig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get license manager
|
|
|
|
|
*/
|
|
|
|
|
public function getLicenseManager(): LicenseManager
|
|
|
|
|
{
|
|
|
|
|
return $this->licenseManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render a Twig template
|
|
|
|
|
*/
|
|
|
|
|
public function render(string $template, array $context = []): string
|
|
|
|
|
{
|
|
|
|
|
return $this->twig->render($template, $context);
|
|
|
|
|
}
|
2026-01-22 18:32:17 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Show admin notice when plugin is unlicensed
|
|
|
|
|
*/
|
|
|
|
|
public function showUnlicensedNotice(): void
|
|
|
|
|
{
|
|
|
|
|
$settingsUrl = admin_url('admin.php?page=wc-settings&tab=licensed_product');
|
|
|
|
|
?>
|
|
|
|
|
<div class="notice notice-warning is-dismissible">
|
|
|
|
|
<p>
|
|
|
|
|
<strong><?php esc_html_e('WC Licensed Product', 'wc-licensed-product'); ?>:</strong>
|
|
|
|
|
<?php esc_html_e('Plugin license is not configured or invalid. Frontend features are disabled.', 'wc-licensed-product'); ?>
|
|
|
|
|
<a href="<?php echo esc_url($settingsUrl); ?>"><?php esc_html_e('Configure License', 'wc-licensed-product'); ?></a>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<?php
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the plugin license checker instance
|
|
|
|
|
*/
|
|
|
|
|
public function getLicenseChecker(): PluginLicenseChecker
|
|
|
|
|
{
|
|
|
|
|
return PluginLicenseChecker::getInstance();
|
|
|
|
|
}
|
2026-01-21 18:55:18 +01:00
|
|
|
}
|