Files
wc-licensed-product/src/Plugin.php

352 lines
12 KiB
PHP
Raw Normal View History

<?php
/**
* Main Plugin class
*
* @package Jeremias\WcLicensedProduct
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct;
use Jeremias\WcLicensedProduct\Admin\AdminController;
use Jeremias\WcLicensedProduct\Admin\DashboardWidgetController;
use Jeremias\WcLicensedProduct\Admin\DownloadWidgetController;
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
use Jeremias\WcLicensedProduct\Api\RestApiController;
use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration;
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
use Jeremias\WcLicensedProduct\Checkout\StoreApiExtension;
use Jeremias\WcLicensedProduct\Email\LicenseEmailController;
use Jeremias\WcLicensedProduct\Frontend\AccountController;
use Jeremias\WcLicensedProduct\Frontend\DownloadController;
use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
use Jeremias\WcLicensedProduct\Product\VersionManager;
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;
/**
* Version manager
*/
private VersionManager $versionManager;
/**
* Download controller
*/
private DownloadController $downloadController;
/**
* Get singleton instance
*/
public static function instance(): Plugin
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get singleton instance (alias for instance())
*/
public static function getInstance(): Plugin
{
return self::instance();
}
/**
* 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',
'auto_reload' => true, // Always check for template changes
'autoescape' => 'html', // Explicitly enable HTML autoescape for XSS protection
]);
// 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();
$this->versionManager = new VersionManager();
// Check plugin license
$licenseChecker = PluginLicenseChecker::getInstance();
$isLicensed = $licenseChecker->isLicenseValid();
// Always initialize product type (needed for existing orders)
new LicensedProductType();
// 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
new RestApiController($this->licenseManager);
new LicenseEmailController($this->licenseManager);
// Initialize response signing if server secret is configured
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') {
(new ResponseSigner())->register();
}
// Admin always available
if (is_admin()) {
new AdminController($this->twig, $this->licenseManager);
new VersionAdminController($this->versionManager);
new OrderLicenseController($this->licenseManager);
new SettingsController();
new DashboardWidgetController($this->licenseManager);
new DownloadWidgetController($this->versionManager);
// Show admin notice if unlicensed and not on localhost
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
add_action('admin_notices', [$this, 'showUnlicensedNotice']);
}
}
}
/**
* 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());
}
);
}
});
}
/**
* Register plugin hooks
*/
private function registerHooks(): void
{
// 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']);
// Also hook into payment complete for immediate license generation
add_action('woocommerce_payment_complete', [$this, 'onOrderCompleted']);
}
}
/**
* Handle order completion - generate licenses
*/
public function onOrderCompleted(int $orderId): void
{
$order = wc_get_order($orderId);
if (!$order) {
return;
}
// 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 (and variation ID for variable products)
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
$productId = (int) $item['product_id'];
$variationId = isset($item['variation_id']) ? (int) $item['variation_id'] : 0;
$key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
$domainsByProduct[$key] = [
'domains' => $item['domains'],
'variation_id' => $variationId,
];
}
}
// Generate licenses for each licensed product
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue;
}
// Get the parent product ID (for variations, this is the main product)
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
$variationId = $item->get_variation_id();
// Look up domains - first try with variation, then without
$key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
$domainInfo = $domainsByProduct[$key] ?? $domainsByProduct[(string) $productId] ?? null;
$domains = $domainInfo['domains'] ?? [];
// Generate a license for each domain
foreach ($domains as $domain) {
if (!empty($domain)) {
$this->licenseManager->generateLicense(
$orderId,
$productId,
$customerId,
$domain,
$variationId > 0 ? $variationId : null
);
}
}
}
}
/**
* 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 && $this->licenseManager->isLicensedProduct($product)) {
// Get the parent product ID (for variations, this is the main product)
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
$variationId = $item->get_variation_id();
$this->licenseManager->generateLicense(
$order->get_id(),
$productId,
$order->get_customer_id(),
$domain,
$variationId > 0 ? $variationId : null
);
}
}
}
/**
* 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);
}
/**
* 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();
}
}