You've already forked wc-licensed-product
Implement version 0.0.1 - Licensed Product type for WooCommerce
Add complete plugin infrastructure for selling software with license keys: - New "Licensed Product" WooCommerce product type - License key generation (XXXX-XXXX-XXXX-XXXX format) on order completion - Domain-based license validation system - REST API endpoints (validate, status, activate, deactivate) - Customer My Account "Licenses" page - Admin license management under WooCommerce > Licenses - Checkout domain field for licensed products - Custom database tables for licenses and product versions - Twig template engine integration - Full i18n support with German (de_CH) translation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
# Linked wordpress core and plugin folder
|
||||
wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
|
||||
48
CHANGELOG.md
Normal file
48
CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.1] - 2024-01-21
|
||||
|
||||
### Added
|
||||
|
||||
- Initial plugin structure with WordPress Plugin API integration
|
||||
- New WooCommerce product type "Licensed Product" for selling software licenses
|
||||
- License key generation with format XXXX-XXXX-XXXX-XXXX on order completion
|
||||
- Domain-based license validation (licenses bound to specific domains)
|
||||
- REST API endpoints for license management:
|
||||
- `POST /wp-json/wc-licensed-product/v1/validate` - Validate license for domain
|
||||
- `POST /wp-json/wc-licensed-product/v1/status` - Check license status
|
||||
- `POST /wp-json/wc-licensed-product/v1/activate` - Activate license on domain
|
||||
- `POST /wp-json/wc-licensed-product/v1/deactivate` - Deactivate license
|
||||
- Checkout domain field for licensed products
|
||||
- Customer account page "Licenses" to view purchased licenses
|
||||
- Admin interface for license management (WooCommerce > Licenses)
|
||||
- License settings per product:
|
||||
- Maximum activations per license
|
||||
- License validity period (days or lifetime)
|
||||
- Optional binding to major software version
|
||||
- Current version tracking
|
||||
- Custom database tables for licenses and product versions
|
||||
- Twig template engine integration for views
|
||||
- Full internationalization support (i18n)
|
||||
- German (Switzerland, formal) translation (de_CH)
|
||||
- WooCommerce HPOS compatibility
|
||||
- Responsive frontend license table
|
||||
|
||||
### Technical
|
||||
|
||||
- PHP 8.3+ required
|
||||
- WooCommerce 10.0+ required
|
||||
- PSR-4 autoloading via Composer
|
||||
- Twig 3.0 template engine
|
||||
- WordPress REST API integration
|
||||
- Custom WooCommerce product type extending WC_Product
|
||||
|
||||
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.1...HEAD
|
||||
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/releases/tag/v0.0.1
|
||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# WC Licensed Product
|
||||
|
||||
A WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||
|
||||
## Description
|
||||
|
||||
WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, enabling you to sell software with automatically generated license keys. Licenses are bound to specific domains and can be validated through a REST API.
|
||||
|
||||
## Features
|
||||
|
||||
- **Licensed Product Type**: New WooCommerce product type for software sales
|
||||
- **Automatic License Generation**: License keys generated on order completion
|
||||
- **Domain Binding**: Licenses are bound to customer-specified domains
|
||||
- **REST API**: Public endpoints for license validation and management
|
||||
- **Customer Account**: Customers can view their licenses in My Account
|
||||
- **Admin Management**: Full CRUD interface for license management
|
||||
- **Version Binding**: Optional binding to major software versions
|
||||
- **Expiration Support**: Set license validity periods or lifetime licenses
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress 6.0 or higher
|
||||
- WooCommerce 10.0 or higher
|
||||
- PHP 8.3 or higher
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the `wc-licensed-product` folder to `/wp-content/plugins/`
|
||||
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
3. The plugin will create necessary database tables on activation
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Licensed Product
|
||||
|
||||
1. Go to Products > Add New
|
||||
2. Select "Licensed Product" from the product type dropdown
|
||||
3. Configure the product price in the General tab
|
||||
4. Set license options in the "License Settings" tab:
|
||||
- **Max Activations**: Number of domains allowed per license
|
||||
- **License Validity**: Days until expiration (empty = lifetime)
|
||||
- **Bind to Major Version**: Lock license to current major version
|
||||
- **Current Version**: Your software's current version
|
||||
|
||||
### Customer Checkout
|
||||
|
||||
When a customer purchases a licensed product, they must enter the domain where they will use the license during checkout.
|
||||
|
||||
### Viewing Licenses
|
||||
|
||||
- **Customers**: My Account > Licenses
|
||||
- **Administrators**: WooCommerce > Licenses
|
||||
|
||||
## REST API
|
||||
|
||||
### Validate License
|
||||
|
||||
```http
|
||||
POST /wp-json/wc-licensed-product/v1/validate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||
"domain": "example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Check Status
|
||||
|
||||
```http
|
||||
POST /wp-json/wc-licensed-product/v1/status
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"license_key": "XXXX-XXXX-XXXX-XXXX"
|
||||
}
|
||||
```
|
||||
|
||||
### Activate License
|
||||
|
||||
```http
|
||||
POST /wp-json/wc-licensed-product/v1/activate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||
"domain": "newdomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Deactivate License
|
||||
|
||||
```http
|
||||
POST /wp-json/wc-licensed-product/v1/deactivate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||
"domain": "example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## License Statuses
|
||||
|
||||
- **Active**: License is valid and usable
|
||||
- **Inactive**: License has been deactivated
|
||||
- **Expired**: License validity period has ended
|
||||
- **Revoked**: License has been manually revoked by admin
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please visit:
|
||||
<https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues>
|
||||
|
||||
## Author
|
||||
|
||||
Marco Graetsch
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later
|
||||
91
assets/css/admin.css
Normal file
91
assets/css/admin.css
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* WC Licensed Product - Admin Styles
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
/* License Status Badges */
|
||||
.license-status {
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.5em;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.license-status-active {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.license-status-inactive {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.license-status-expired {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.license-status-revoked {
|
||||
background-color: #d6d8db;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
/* License Table */
|
||||
.wp-list-table code {
|
||||
font-family: monospace;
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* License Product Tab */
|
||||
#woocommerce-product-data .show_if_licensed {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
#woocommerce-product-data .hide_if_licensed {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.wp-list-table .button-link-delete {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.wp-list-table .button-link-delete:hover {
|
||||
color: #dc3232;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.tablenav-pages .pagination-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.tablenav-pages .paging-input {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
/* Orders Column */
|
||||
.column-license {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.column-license .dashicons {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
.column-license .dashicons-warning {
|
||||
color: #dba617;
|
||||
}
|
||||
|
||||
.column-license .dashicons-admin-network {
|
||||
color: #2271b1;
|
||||
}
|
||||
124
assets/css/frontend.css
Normal file
124
assets/css/frontend.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* WC Licensed Product - Frontend Styles
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
/* License Status Badges */
|
||||
.license-status {
|
||||
display: inline-block;
|
||||
padding: 0.25em 0.6em;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.license-status-active {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.license-status-inactive {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.license-status-expired {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.license-status-revoked {
|
||||
background-color: #d6d8db;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
/* License Table */
|
||||
.woocommerce-licenses-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table th,
|
||||
.woocommerce-licenses-table td {
|
||||
padding: 0.75em;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table th {
|
||||
font-weight: 600;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table code {
|
||||
font-family: monospace;
|
||||
background-color: #f5f5f5;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Domain Field */
|
||||
#licensed-product-domain-field {
|
||||
margin-top: 2em;
|
||||
padding: 1.5em;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#licensed-product-domain-field h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
#licensed-product-domain-field .description {
|
||||
display: block;
|
||||
margin-top: 0.5em;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 768px) {
|
||||
.woocommerce-licenses-table,
|
||||
.woocommerce-licenses-table thead,
|
||||
.woocommerce-licenses-table tbody,
|
||||
.woocommerce-licenses-table th,
|
||||
.woocommerce-licenses-table td,
|
||||
.woocommerce-licenses-table tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table thead tr {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table tr {
|
||||
border: 1px solid #e5e5e5;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table td {
|
||||
border: none;
|
||||
position: relative;
|
||||
padding-left: 50%;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table td:before {
|
||||
content: attr(data-title);
|
||||
position: absolute;
|
||||
left: 0.75em;
|
||||
width: 45%;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
31
composer.json
Normal file
31
composer.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "magdev/wc-licensed-product",
|
||||
"description": "WooCommerce plugin to sell software products using license keys",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Graetsch",
|
||||
"email": "magdev3.0@gmail.com",
|
||||
"homepage": "https://src.bundespruefstelle.ch/magdev"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3.0",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jeremias\\WcLicensedProduct\\": "src/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.3.0"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
338
composer.lock
generated
Normal file
338
composer.lock
generated
Normal file
@@ -0,0 +1,338 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3b63b77b19677953867f471c141fee05",
|
||||
"packages": [
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.6-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-12-23T08:48:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.22.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Resources/core.php",
|
||||
"src/Resources/debug.php",
|
||||
"src/Resources/escaper.php",
|
||||
"src/Resources/string_loader.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Twig\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Twig Team",
|
||||
"role": "Contributors"
|
||||
},
|
||||
{
|
||||
"name": "Armin Ronacher",
|
||||
"email": "armin.ronacher@active-4.com",
|
||||
"role": "Project Founder"
|
||||
}
|
||||
],
|
||||
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.22.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-14T11:28:47+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.3.0"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"platform-overrides": {
|
||||
"php": "8.3.0"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
256
languages/wc-licensed-product-de_CH.po
Normal file
256
languages/wc-licensed-product-de_CH.po
Normal file
@@ -0,0 +1,256 @@
|
||||
# German (Switzerland, formal) translation for WC Licensed Product
|
||||
# Copyright (C) 2024 Marco Graetsch
|
||||
# This file is distributed under the GPL-2.0-or-later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WC Licensed Product 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
|
||||
"PO-Revision-Date: 2024-01-01T00:00:00+00:00\n"
|
||||
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
|
||||
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
|
||||
"Language: de_CH\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: wc-licensed-product.php
|
||||
msgid "%s requires WooCommerce to be installed and active."
|
||||
msgstr "%s benötigt WooCommerce als installierte und aktivierte Erweiterung."
|
||||
|
||||
#: wc-licensed-product.php
|
||||
msgid "WC Licensed Product requires WooCommerce to be installed and active."
|
||||
msgstr "WC Licensed Product benötigt WooCommerce als installierte und aktivierte Erweiterung."
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Licensed Product"
|
||||
msgstr "Lizenziertes Produkt"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "License Settings"
|
||||
msgstr "Lizenz-Einstellungen"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Max Activations"
|
||||
msgstr "Max. Aktivierungen"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Maximum number of domain activations per license. Default: 1"
|
||||
msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: 1"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "License Validity (Days)"
|
||||
msgstr "Lizenz-Gültigkeit (Tage)"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Number of days the license is valid. Leave empty for lifetime license."
|
||||
msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für eine lebenslange Lizenz."
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Bind to Major Version"
|
||||
msgstr "An Hauptversion binden"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "If enabled, licenses are bound to the major version at purchase time."
|
||||
msgstr "Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt gebunden."
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Current Version"
|
||||
msgstr "Aktuelle Version"
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Current software version (e.g., 1.0.0)"
|
||||
msgstr "Aktuelle Software-Version (z.B. 1.0.0)"
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "License key not found."
|
||||
msgstr "Lizenzschlüssel nicht gefunden."
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license has been revoked."
|
||||
msgstr "Diese Lizenz wurde widerrufen."
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license has expired."
|
||||
msgstr "Diese Lizenz ist abgelaufen."
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license is inactive."
|
||||
msgstr "Diese Lizenz ist inaktiv."
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license is not valid for this domain."
|
||||
msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "This license is not valid."
|
||||
msgstr "Diese Lizenz ist ungültig."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License is already activated for this domain."
|
||||
msgstr "Die Lizenz ist bereits für diese Domain aktiviert."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Maximum number of activations reached."
|
||||
msgstr "Maximale Anzahl der Aktivierungen erreicht."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Failed to activate license."
|
||||
msgstr "Lizenz konnte nicht aktiviert werden."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License activated successfully."
|
||||
msgstr "Lizenz erfolgreich aktiviert."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License is not activated for this domain."
|
||||
msgstr "Die Lizenz ist für diese Domain nicht aktiviert."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Failed to deactivate license."
|
||||
msgstr "Lizenz konnte nicht deaktiviert werden."
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License deactivated successfully."
|
||||
msgstr "Lizenz erfolgreich deaktiviert."
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "License Domain"
|
||||
msgstr "Lizenz-Domain"
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Domain for License Activation"
|
||||
msgstr "Domain für Lizenz-Aktivierung"
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "required"
|
||||
msgstr "erforderlich"
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "example.com"
|
||||
msgstr "beispiel.ch"
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Enter the domain where you will use this license (without http:// or www)."
|
||||
msgstr "Geben Sie die Domain ein, auf der Sie diese Lizenz verwenden möchten (ohne http:// oder www)."
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Please enter a domain for your license activation."
|
||||
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz-Aktivierung ein."
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Please enter a valid domain name."
|
||||
msgstr "Bitte geben Sie einen gültigen Domain-Namen ein."
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "License Domain:"
|
||||
msgstr "Lizenz-Domain:"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Please log in to view your licenses."
|
||||
msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Licenses"
|
||||
msgstr "Lizenzen"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Unknown Product"
|
||||
msgstr "Unbekanntes Produkt"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "You have no licenses yet."
|
||||
msgstr "Sie haben noch keine Lizenzen."
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "License Key"
|
||||
msgstr "Lizenzschlüssel"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Product"
|
||||
msgstr "Produkt"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Expires"
|
||||
msgstr "Läuft ab"
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Never"
|
||||
msgstr "Nie"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Security check failed."
|
||||
msgstr "Sicherheitsüberprüfung fehlgeschlagen."
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License updated successfully."
|
||||
msgstr "Lizenz erfolgreich aktualisiert."
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License deleted successfully."
|
||||
msgstr "Lizenz erfolgreich gelöscht."
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License revoked successfully."
|
||||
msgstr "Lizenz erfolgreich widerrufen."
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Unknown"
|
||||
msgstr "Unbekannt"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Guest"
|
||||
msgstr "Gast"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Customer"
|
||||
msgstr "Kunde"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "No licenses found."
|
||||
msgstr "Keine Lizenzen gefunden."
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Are you sure?"
|
||||
msgstr "Sind Sie sicher?"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Revoke"
|
||||
msgstr "Widerrufen"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Are you sure you want to delete this license?"
|
||||
msgstr "Sind Sie sicher, dass Sie diese Lizenz löschen möchten?"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License"
|
||||
msgstr "Lizenz"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "No domain specified"
|
||||
msgstr "Keine Domain angegeben"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Total licenses:"
|
||||
msgstr "Lizenzen insgesamt:"
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "of"
|
||||
msgstr "von"
|
||||
253
languages/wc-licensed-product.pot
Normal file
253
languages/wc-licensed-product.pot
Normal file
@@ -0,0 +1,253 @@
|
||||
# Copyright (C) 2024 Marco Graetsch
|
||||
# This file is distributed under the GPL-2.0-or-later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WC Licensed Product 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
|
||||
"POT-Creation-Date: 2024-01-01T00:00:00+00:00\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
#: wc-licensed-product.php
|
||||
msgid "%s requires WooCommerce to be installed and active."
|
||||
msgstr ""
|
||||
|
||||
#: wc-licensed-product.php
|
||||
msgid "WC Licensed Product requires WooCommerce to be installed and active."
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Licensed Product"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "License Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Max Activations"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Maximum number of domain activations per license. Default: 1"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "License Validity (Days)"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Number of days the license is valid. Leave empty for lifetime license."
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Bind to Major Version"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "If enabled, licenses are bound to the major version at purchase time."
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Current Version"
|
||||
msgstr ""
|
||||
|
||||
#: src/Product/LicensedProductType.php
|
||||
msgid "Current software version (e.g., 1.0.0)"
|
||||
msgstr ""
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "License key not found."
|
||||
msgstr ""
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license has been revoked."
|
||||
msgstr ""
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license has expired."
|
||||
msgstr ""
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license is inactive."
|
||||
msgstr ""
|
||||
|
||||
#: src/License/LicenseManager.php
|
||||
msgid "This license is not valid for this domain."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "This license is not valid."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License is already activated for this domain."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Maximum number of activations reached."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Failed to activate license."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License activated successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License is not activated for this domain."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "Failed to deactivate license."
|
||||
msgstr ""
|
||||
|
||||
#: src/Api/RestApiController.php
|
||||
msgid "License deactivated successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "License Domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Domain for License Activation"
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "example.com"
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Enter the domain where you will use this license (without http:// or www)."
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Please enter a domain for your license activation."
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "Please enter a valid domain name."
|
||||
msgstr ""
|
||||
|
||||
#: src/Checkout/CheckoutController.php
|
||||
msgid "License Domain:"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Please log in to view your licenses."
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Licenses"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Unknown Product"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "You have no licenses yet."
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "License Key"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Product"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
#: src/Frontend/AccountController.php
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Security check failed."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License updated successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License revoked successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Guest"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Customer"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "No licenses found."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Are you sure you want to delete this license?"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "License"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "No domain specified"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "Total licenses:"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/AdminController.php
|
||||
msgid "of"
|
||||
msgstr ""
|
||||
412
src/Admin/AdminController.php
Normal file
412
src/Admin/AdminController.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?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 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']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin styles and scripts
|
||||
*/
|
||||
public function enqueueStyles(string $hook): void
|
||||
{
|
||||
if ($hook !== 'woocommerce_page_wc-licenses') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wc-licensed-product-admin',
|
||||
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/admin.css',
|
||||
[],
|
||||
WC_LICENSED_PRODUCT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render licenses admin page
|
||||
*/
|
||||
public function renderLicensesPage(): void
|
||||
{
|
||||
$page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
|
||||
$perPage = 20;
|
||||
|
||||
$licenses = $this->licenseManager->getAllLicenses($page, $perPage);
|
||||
$totalLicenses = $this->licenseManager->getLicenseCount();
|
||||
$totalPages = ceil($totalLicenses / $perPage);
|
||||
|
||||
// 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 : '',
|
||||
];
|
||||
}
|
||||
|
||||
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(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to PHP template
|
||||
$this->renderLicensesPageFallback($enrichedLicenses, $page, $totalPages, $totalLicenses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')];
|
||||
}
|
||||
|
||||
return $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback render for licenses page
|
||||
*/
|
||||
private function renderLicensesPageFallback(array $enrichedLicenses, int $page, int $totalPages, int $totalLicenses): void
|
||||
{
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h1>
|
||||
|
||||
<?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; ?>
|
||||
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<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('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="7"><?php esc_html_e('No licenses found.', 'wc-licensed-product'); ?></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($enrichedLicenses as $item): ?>
|
||||
<tr>
|
||||
<td><code><?php echo esc_html($item['license']->getLicenseKey()); ?></code></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><?php echo esc_html($item['license']->getDomain()); ?></td>
|
||||
<td>
|
||||
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
|
||||
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$expiresAt = $item['license']->getExpiresAt();
|
||||
echo $expiresAt
|
||||
? esc_html($expiresAt->format(get_option('date_format')))
|
||||
: esc_html__('Never', 'wc-licensed-product');
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
|
||||
<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'
|
||||
)); ?>" class="button button-small" onclick="return confirm('<?php esc_attr_e('Are you sure?', 'wc-licensed-product'); ?>')">
|
||||
<?php esc_html_e('Revoke', 'wc-licensed-product'); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<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="button button-small button-link-delete" 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>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="tablenav bottom">
|
||||
<div class="tablenav-pages">
|
||||
<?php
|
||||
echo paginate_links([
|
||||
'base' => admin_url('admin.php?page=wc-licenses&paged=%#%'),
|
||||
'format' => '',
|
||||
'current' => $page,
|
||||
'total' => $totalPages,
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
271
src/Api/RestApiController.php
Normal file
271
src/Api/RestApiController.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Api
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Api;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* Handles REST API endpoints for license validation
|
||||
*/
|
||||
final class RestApiController
|
||||
{
|
||||
private const NAMESPACE = 'wc-licensed-product/v1';
|
||||
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
public function __construct(LicenseManager $licenseManager)
|
||||
{
|
||||
$this->licenseManager = $licenseManager;
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
add_action('rest_api_init', [$this, 'registerRoutes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public function registerRoutes(): void
|
||||
{
|
||||
// Validate license endpoint (public)
|
||||
register_rest_route(self::NAMESPACE, '/validate', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'validateLicense'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'license_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => function ($value): bool {
|
||||
return !empty($value) && strlen($value) <= 64;
|
||||
},
|
||||
],
|
||||
'domain' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => function ($value): bool {
|
||||
return !empty($value) && strlen($value) <= 255;
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Check license status endpoint (public)
|
||||
register_rest_route(self::NAMESPACE, '/status', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'checkStatus'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'license_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Activate license on domain endpoint (public)
|
||||
register_rest_route(self::NAMESPACE, '/activate', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'activateLicense'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'license_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'domain' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Deactivate license endpoint (public)
|
||||
register_rest_route(self::NAMESPACE, '/deactivate', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'deactivateLicense'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'license_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'domain' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate license endpoint
|
||||
*/
|
||||
public function validateLicense(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$licenseKey = $request->get_param('license_key');
|
||||
$domain = $request->get_param('domain');
|
||||
|
||||
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||
|
||||
$statusCode = $result['valid'] ? 200 : 403;
|
||||
|
||||
return new WP_REST_Response($result, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check license status endpoint
|
||||
*/
|
||||
public function checkStatus(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$licenseKey = $request->get_param('license_key');
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
return new WP_REST_Response([
|
||||
'valid' => false,
|
||||
'error' => 'license_not_found',
|
||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'valid' => $license->isValid(),
|
||||
'status' => $license->getStatus(),
|
||||
'domain' => $license->getDomain(),
|
||||
'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
|
||||
'activations_count' => $license->getActivationsCount(),
|
||||
'max_activations' => $license->getMaxActivations(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate license on domain endpoint
|
||||
*/
|
||||
public function activateLicense(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$licenseKey = $request->get_param('license_key');
|
||||
$domain = $request->get_param('domain');
|
||||
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'license_not_found',
|
||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (!$license->isValid()) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'license_invalid',
|
||||
'message' => __('This license is not valid.', 'wc-licensed-product'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
|
||||
// Check if already activated on this domain
|
||||
if ($license->getDomain() === $normalizedDomain) {
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if can activate on another domain
|
||||
if (!$license->canActivate()) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'max_activations_reached',
|
||||
'message' => __('Maximum number of activations reached.', 'wc-licensed-product'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Update domain (in this simple implementation, we replace the domain)
|
||||
$success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);
|
||||
|
||||
if (!$success) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'activation_failed',
|
||||
'message' => __('Failed to activate license.', 'wc-licensed-product'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('License activated successfully.', 'wc-licensed-product'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate license endpoint
|
||||
*/
|
||||
public function deactivateLicense(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$licenseKey = $request->get_param('license_key');
|
||||
$domain = $request->get_param('domain');
|
||||
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'license_not_found',
|
||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
|
||||
// Verify domain matches
|
||||
if ($license->getDomain() !== $normalizedDomain) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'domain_mismatch',
|
||||
'message' => __('License is not activated for this domain.', 'wc-licensed-product'),
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Set status to inactive
|
||||
$success = $this->licenseManager->updateLicenseStatus($license->getId(), 'inactive');
|
||||
|
||||
if (!$success) {
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'deactivation_failed',
|
||||
'message' => __('Failed to deactivate license.', 'wc-licensed-product'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('License deactivated successfully.', 'wc-licensed-product'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
207
src/Checkout/CheckoutController.php
Normal file
207
src/Checkout/CheckoutController.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/**
|
||||
* Checkout Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Checkout
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Checkout;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
|
||||
/**
|
||||
* Handles checkout modifications for licensed products
|
||||
*/
|
||||
final class CheckoutController
|
||||
{
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
public function __construct(LicenseManager $licenseManager)
|
||||
{
|
||||
$this->licenseManager = $licenseManager;
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
// Add domain field to checkout
|
||||
add_action('woocommerce_after_order_notes', [$this, 'addDomainField']);
|
||||
|
||||
// Validate domain field
|
||||
add_action('woocommerce_checkout_process', [$this, 'validateDomainField']);
|
||||
|
||||
// Save domain field to order meta
|
||||
add_action('woocommerce_checkout_update_order_meta', [$this, 'saveDomainField']);
|
||||
|
||||
// Display domain in order details (admin)
|
||||
add_action('woocommerce_admin_order_data_after_billing_address', [$this, 'displayDomainInAdmin']);
|
||||
|
||||
// Display domain in order email
|
||||
add_action('woocommerce_email_after_order_table', [$this, 'displayDomainInEmail'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cart contains licensed products
|
||||
*/
|
||||
private function cartHasLicensedProducts(): bool
|
||||
{
|
||||
if (!WC()->cart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add domain field to checkout form
|
||||
*/
|
||||
public function addDomainField(): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div id="licensed-product-domain-field">
|
||||
<h3><?php esc_html_e('License Domain', 'wc-licensed-product'); ?></h3>
|
||||
<p class="form-row form-row-wide">
|
||||
<label for="licensed_product_domain">
|
||||
<?php esc_html_e('Domain for License Activation', 'wc-licensed-product'); ?>
|
||||
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input-text"
|
||||
name="licensed_product_domain"
|
||||
id="licensed_product_domain"
|
||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
||||
value="<?php echo esc_attr(WC()->checkout->get_value('licensed_product_domain')); ?>"
|
||||
/>
|
||||
<span class="description">
|
||||
<?php esc_html_e('Enter the domain where you will use this license (without http:// or www).', 'wc-licensed-product'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain field during checkout
|
||||
*/
|
||||
public function validateDomainField(): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = isset($_POST['licensed_product_domain'])
|
||||
? sanitize_text_field($_POST['licensed_product_domain'])
|
||||
: '';
|
||||
|
||||
if (empty($domain)) {
|
||||
wc_add_notice(
|
||||
__('Please enter a domain for your license activation.', 'wc-licensed-product'),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!$this->isValidDomain($normalizedDomain)) {
|
||||
wc_add_notice(
|
||||
__('Please enter a valid domain name.', 'wc-licensed-product'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save domain field to order meta
|
||||
*/
|
||||
public function saveDomainField(int $orderId): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_POST['licensed_product_domain']) && !empty($_POST['licensed_product_domain'])) {
|
||||
$domain = sanitize_text_field($_POST['licensed_product_domain']);
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
|
||||
$order = wc_get_order($orderId);
|
||||
if ($order) {
|
||||
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domain in admin order view
|
||||
*/
|
||||
public function displayDomainInAdmin(\WC_Order $order): void
|
||||
{
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
if (!$domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<p>
|
||||
<strong><?php esc_html_e('License Domain:', 'wc-licensed-product'); ?></strong>
|
||||
<?php echo esc_html($domain); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domain in order emails
|
||||
*/
|
||||
public function displayDomainInEmail(\WC_Order $order, bool $sentToAdmin, bool $plainText): void
|
||||
{
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
if (!$domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($plainText) {
|
||||
echo "\n" . esc_html__('License Domain:', 'wc-licensed-product') . ' ' . esc_html($domain) . "\n";
|
||||
} else {
|
||||
?>
|
||||
<p>
|
||||
<strong><?php esc_html_e('License Domain:', 'wc-licensed-product'); ?></strong>
|
||||
<?php echo esc_html($domain); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain format
|
||||
*/
|
||||
private function isValidDomain(string $domain): bool
|
||||
{
|
||||
// Basic domain validation
|
||||
if (strlen($domain) > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for valid domain pattern
|
||||
$pattern = '/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
|
||||
|
||||
return (bool) preg_match($pattern, $domain);
|
||||
}
|
||||
}
|
||||
187
src/Frontend/AccountController.php
Normal file
187
src/Frontend/AccountController.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend Account Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Frontend
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Frontend;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Handles customer account pages for viewing licenses
|
||||
*/
|
||||
final class AccountController
|
||||
{
|
||||
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 licenses endpoint
|
||||
add_action('init', [$this, 'addLicensesEndpoint']);
|
||||
|
||||
// Add licenses menu item
|
||||
add_filter('woocommerce_account_menu_items', [$this, 'addLicensesMenuItem']);
|
||||
|
||||
// Add licenses endpoint content
|
||||
add_action('woocommerce_account_licenses_endpoint', [$this, 'displayLicensesContent']);
|
||||
|
||||
// Enqueue frontend styles
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueueStyles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add licenses endpoint for My Account
|
||||
*/
|
||||
public function addLicensesEndpoint(): void
|
||||
{
|
||||
add_rewrite_endpoint('licenses', EP_ROOT | EP_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add licenses menu item to My Account navigation
|
||||
*/
|
||||
public function addLicensesMenuItem(array $items): array
|
||||
{
|
||||
// Insert licenses after orders
|
||||
$newItems = [];
|
||||
foreach ($items as $key => $value) {
|
||||
$newItems[$key] = $value;
|
||||
if ($key === 'orders') {
|
||||
$newItems['licenses'] = __('Licenses', 'wc-licensed-product');
|
||||
}
|
||||
}
|
||||
|
||||
return $newItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display licenses content in My Account
|
||||
*/
|
||||
public function displayLicensesContent(): void
|
||||
{
|
||||
$customerId = get_current_user_id();
|
||||
if (!$customerId) {
|
||||
echo '<p>' . esc_html__('Please log in to view your licenses.', 'wc-licensed-product') . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$licenses = $this->licenseManager->getLicensesByCustomer($customerId);
|
||||
|
||||
// Enrich licenses with product data
|
||||
$enrichedLicenses = [];
|
||||
foreach ($licenses as $license) {
|
||||
$product = wc_get_product($license->getProductId());
|
||||
$order = wc_get_order($license->getOrderId());
|
||||
|
||||
$enrichedLicenses[] = [
|
||||
'license' => $license,
|
||||
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
||||
'product_url' => $product ? $product->get_permalink() : '',
|
||||
'order_number' => $order ? $order->get_order_number() : '',
|
||||
'order_url' => $order ? $order->get_view_order_url() : '',
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
echo $this->twig->render('frontend/licenses.html.twig', [
|
||||
'licenses' => $enrichedLicenses,
|
||||
'has_licenses' => !empty($enrichedLicenses),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to PHP template if Twig fails
|
||||
$this->displayLicensesFallback($enrichedLicenses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback display method if Twig is unavailable
|
||||
*/
|
||||
private function displayLicensesFallback(array $enrichedLicenses): void
|
||||
{
|
||||
if (empty($enrichedLicenses)) {
|
||||
echo '<p>' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<table class="woocommerce-licenses-table shop_table shop_table_responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<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('Domain', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($enrichedLicenses as $item): ?>
|
||||
<tr>
|
||||
<td data-title="<?php esc_attr_e('License Key', 'wc-licensed-product'); ?>">
|
||||
<code><?php echo esc_html($item['license']->getLicenseKey()); ?></code>
|
||||
</td>
|
||||
<td data-title="<?php esc_attr_e('Product', 'wc-licensed-product'); ?>">
|
||||
<?php if ($item['product_url']): ?>
|
||||
<a href="<?php echo esc_url($item['product_url']); ?>">
|
||||
<?php echo esc_html($item['product_name']); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php echo esc_html($item['product_name']); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td data-title="<?php esc_attr_e('Domain', 'wc-licensed-product'); ?>">
|
||||
<?php echo esc_html($item['license']->getDomain()); ?>
|
||||
</td>
|
||||
<td data-title="<?php esc_attr_e('Status', 'wc-licensed-product'); ?>">
|
||||
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
|
||||
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-title="<?php esc_attr_e('Expires', 'wc-licensed-product'); ?>">
|
||||
<?php
|
||||
$expiresAt = $item['license']->getExpiresAt();
|
||||
echo $expiresAt
|
||||
? esc_html($expiresAt->format(get_option('date_format')))
|
||||
: esc_html__('Never', 'wc-licensed-product');
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend styles
|
||||
*/
|
||||
public function enqueueStyles(): void
|
||||
{
|
||||
if (!is_account_page()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wc-licensed-product-frontend',
|
||||
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/frontend.css',
|
||||
[],
|
||||
WC_LICENSED_PRODUCT_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
138
src/Installer.php
Normal file
138
src/Installer.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Installer
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct;
|
||||
|
||||
/**
|
||||
* Handles plugin activation and deactivation
|
||||
*/
|
||||
final class Installer
|
||||
{
|
||||
/**
|
||||
* Database table name for licenses
|
||||
*/
|
||||
public const TABLE_LICENSES = 'wc_licensed_product_licenses';
|
||||
|
||||
/**
|
||||
* Database table name for product versions
|
||||
*/
|
||||
public const TABLE_PRODUCT_VERSIONS = 'wc_licensed_product_versions';
|
||||
|
||||
/**
|
||||
* Run on plugin activation
|
||||
*/
|
||||
public static function activate(): void
|
||||
{
|
||||
self::createTables();
|
||||
self::createCacheDir();
|
||||
|
||||
// Set version in options
|
||||
update_option('wc_licensed_product_version', WC_LICENSED_PRODUCT_VERSION);
|
||||
|
||||
// Flush rewrite rules for REST API
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run on plugin deactivation
|
||||
*/
|
||||
public static function deactivate(): void
|
||||
{
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
private static function createTables(): void
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$charsetCollate = $wpdb->get_charset_collate();
|
||||
|
||||
$licensesTable = $wpdb->prefix . self::TABLE_LICENSES;
|
||||
$versionsTable = $wpdb->prefix . self::TABLE_PRODUCT_VERSIONS;
|
||||
|
||||
$sqlLicenses = "CREATE TABLE {$licensesTable} (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
license_key VARCHAR(64) NOT NULL,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
customer_id BIGINT UNSIGNED NOT NULL,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
version_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
status ENUM('active', 'inactive', 'expired', 'revoked') NOT NULL DEFAULT 'active',
|
||||
activations_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
max_activations INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
expires_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY license_key (license_key),
|
||||
KEY order_id (order_id),
|
||||
KEY product_id (product_id),
|
||||
KEY customer_id (customer_id),
|
||||
KEY domain (domain),
|
||||
KEY status (status)
|
||||
) {$charsetCollate};";
|
||||
|
||||
$sqlVersions = "CREATE TABLE {$versionsTable} (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
version VARCHAR(32) NOT NULL,
|
||||
major_version INT UNSIGNED NOT NULL,
|
||||
minor_version INT UNSIGNED NOT NULL,
|
||||
patch_version INT UNSIGNED NOT NULL,
|
||||
release_notes TEXT DEFAULT NULL,
|
||||
download_url VARCHAR(512) DEFAULT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
released_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY product_version (product_id, version),
|
||||
KEY product_id (product_id),
|
||||
KEY major_version (major_version),
|
||||
KEY is_active (is_active)
|
||||
) {$charsetCollate};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta($sqlLicenses);
|
||||
dbDelta($sqlVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Twig cache directory
|
||||
*/
|
||||
private static function createCacheDir(): void
|
||||
{
|
||||
$cacheDir = WP_CONTENT_DIR . '/cache/wc-licensed-product/twig';
|
||||
if (!file_exists($cacheDir)) {
|
||||
wp_mkdir_p($cacheDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get licenses table name
|
||||
*/
|
||||
public static function getLicensesTable(): string
|
||||
{
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . self::TABLE_LICENSES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product versions table name
|
||||
*/
|
||||
public static function getVersionsTable(): string
|
||||
{
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . self::TABLE_PRODUCT_VERSIONS;
|
||||
}
|
||||
}
|
||||
178
src/License/License.php
Normal file
178
src/License/License.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* License Model
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\License;
|
||||
|
||||
/**
|
||||
* License entity model
|
||||
*/
|
||||
class License
|
||||
{
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_INACTIVE = 'inactive';
|
||||
public const STATUS_EXPIRED = 'expired';
|
||||
public const STATUS_REVOKED = 'revoked';
|
||||
|
||||
private int $id;
|
||||
private string $licenseKey;
|
||||
private int $orderId;
|
||||
private int $productId;
|
||||
private int $customerId;
|
||||
private string $domain;
|
||||
private ?int $versionId;
|
||||
private string $status;
|
||||
private int $activationsCount;
|
||||
private int $maxActivations;
|
||||
private ?\DateTimeInterface $expiresAt;
|
||||
private \DateTimeInterface $createdAt;
|
||||
private \DateTimeInterface $updatedAt;
|
||||
|
||||
/**
|
||||
* Create license from database row
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$license = new self();
|
||||
$license->id = (int) $data['id'];
|
||||
$license->licenseKey = $data['license_key'];
|
||||
$license->orderId = (int) $data['order_id'];
|
||||
$license->productId = (int) $data['product_id'];
|
||||
$license->customerId = (int) $data['customer_id'];
|
||||
$license->domain = $data['domain'];
|
||||
$license->versionId = $data['version_id'] ? (int) $data['version_id'] : null;
|
||||
$license->status = $data['status'];
|
||||
$license->activationsCount = (int) $data['activations_count'];
|
||||
$license->maxActivations = (int) $data['max_activations'];
|
||||
$license->expiresAt = $data['expires_at'] ? new \DateTimeImmutable($data['expires_at']) : null;
|
||||
$license->createdAt = new \DateTimeImmutable($data['created_at']);
|
||||
$license->updatedAt = new \DateTimeImmutable($data['updated_at']);
|
||||
|
||||
return $license;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLicenseKey(): string
|
||||
{
|
||||
return $this->licenseKey;
|
||||
}
|
||||
|
||||
public function getOrderId(): int
|
||||
{
|
||||
return $this->orderId;
|
||||
}
|
||||
|
||||
public function getProductId(): int
|
||||
{
|
||||
return $this->productId;
|
||||
}
|
||||
|
||||
public function getCustomerId(): int
|
||||
{
|
||||
return $this->customerId;
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function getVersionId(): ?int
|
||||
{
|
||||
return $this->versionId;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getActivationsCount(): int
|
||||
{
|
||||
return $this->activationsCount;
|
||||
}
|
||||
|
||||
public function getMaxActivations(): int
|
||||
{
|
||||
return $this->maxActivations;
|
||||
}
|
||||
|
||||
public function getExpiresAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->expiresAt;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license is currently valid
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
if ($this->status !== self::STATUS_ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expiresAt !== null && $this->expiresAt < new \DateTimeImmutable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license has expired
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expiresAt !== null && $this->expiresAt < new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license can be activated on another domain
|
||||
*/
|
||||
public function canActivate(): bool
|
||||
{
|
||||
return $this->isValid() && $this->activationsCount < $this->maxActivations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for JSON/API responses
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'license_key' => $this->licenseKey,
|
||||
'order_id' => $this->orderId,
|
||||
'product_id' => $this->productId,
|
||||
'customer_id' => $this->customerId,
|
||||
'domain' => $this->domain,
|
||||
'version_id' => $this->versionId,
|
||||
'status' => $this->status,
|
||||
'activations_count' => $this->activationsCount,
|
||||
'max_activations' => $this->maxActivations,
|
||||
'expires_at' => $this->expiresAt?->format('Y-m-d H:i:s'),
|
||||
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $this->updatedAt->format('Y-m-d H:i:s'),
|
||||
'is_valid' => $this->isValid(),
|
||||
];
|
||||
}
|
||||
}
|
||||
363
src/License/LicenseManager.php
Normal file
363
src/License/LicenseManager.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
/**
|
||||
* License Manager
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\License;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Installer;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
|
||||
|
||||
/**
|
||||
* Manages license operations (CRUD, validation, generation)
|
||||
*/
|
||||
class LicenseManager
|
||||
{
|
||||
/**
|
||||
* Generate a unique license key
|
||||
*/
|
||||
public function generateLicenseKey(): string
|
||||
{
|
||||
// Format: XXXX-XXXX-XXXX-XXXX (32 chars hex, 4 groups)
|
||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$key = '';
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
if ($i > 0) {
|
||||
$key .= '-';
|
||||
}
|
||||
for ($j = 0; $j < 4; $j++) {
|
||||
$key .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a license for a completed order
|
||||
*/
|
||||
public function generateLicense(
|
||||
int $orderId,
|
||||
int $productId,
|
||||
int $customerId,
|
||||
string $domain
|
||||
): ?License {
|
||||
global $wpdb;
|
||||
|
||||
// Check if license already exists for this order and product
|
||||
$existing = $this->getLicenseByOrderAndProduct($orderId, $productId);
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$product = wc_get_product($productId);
|
||||
if (!$product instanceof LicensedProduct) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate unique license key
|
||||
$licenseKey = $this->generateLicenseKey();
|
||||
while ($this->getLicenseByKey($licenseKey)) {
|
||||
$licenseKey = $this->generateLicenseKey();
|
||||
}
|
||||
|
||||
// Calculate expiration date
|
||||
$expiresAt = null;
|
||||
$validityDays = $product->get_validity_days();
|
||||
if ($validityDays !== null && $validityDays > 0) {
|
||||
$expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// Determine version ID if bound to version
|
||||
$versionId = null;
|
||||
if ($product->is_bound_to_version()) {
|
||||
$versionId = $this->getCurrentVersionId($productId);
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->insert(
|
||||
$tableName,
|
||||
[
|
||||
'license_key' => $licenseKey,
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $productId,
|
||||
'customer_id' => $customerId,
|
||||
'domain' => $this->normalizeDomain($domain),
|
||||
'version_id' => $versionId,
|
||||
'status' => License::STATUS_ACTIVE,
|
||||
'activations_count' => 1,
|
||||
'max_activations' => $product->get_max_activations(),
|
||||
'expires_at' => $expiresAt,
|
||||
],
|
||||
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getLicenseById((int) $wpdb->insert_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by ID
|
||||
*/
|
||||
public function getLicenseById(int $id): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare("SELECT * FROM {$tableName} WHERE id = %d", $id),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by license key
|
||||
*/
|
||||
public function getLicenseByKey(string $licenseKey): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare("SELECT * FROM {$tableName} WHERE license_key = %s", $licenseKey),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license by order and product
|
||||
*/
|
||||
public function getLicenseByOrderAndProduct(int $orderId, int $productId): ?License
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d",
|
||||
$orderId,
|
||||
$productId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses for a customer
|
||||
*/
|
||||
public function getLicensesByCustomer(int $customerId): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} WHERE customer_id = %d ORDER BY created_at DESC",
|
||||
$customerId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses (for admin)
|
||||
*/
|
||||
public function getAllLicenses(int $page = 1, int $perPage = 20): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$perPage,
|
||||
$offset
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total license count
|
||||
*/
|
||||
public function getLicenseCount(): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a license key for a domain
|
||||
*/
|
||||
public function validateLicense(string $licenseKey, string $domain): array
|
||||
{
|
||||
$license = $this->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_not_found',
|
||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check license status
|
||||
if ($license->getStatus() === License::STATUS_REVOKED) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_revoked',
|
||||
'message' => __('This license has been revoked.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if ($license->isExpired()) {
|
||||
$this->updateLicenseStatus($license->getId(), License::STATUS_EXPIRED);
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_expired',
|
||||
'message' => __('This license has expired.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($license->getStatus() === License::STATUS_INACTIVE) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'license_inactive',
|
||||
'message' => __('This license is inactive.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
// Check domain
|
||||
$normalizedDomain = $this->normalizeDomain($domain);
|
||||
if ($license->getDomain() !== $normalizedDomain) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'error' => 'domain_mismatch',
|
||||
'message' => __('This license is not valid for this domain.', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => true,
|
||||
'license' => [
|
||||
'product_id' => $license->getProductId(),
|
||||
'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
|
||||
'version_id' => $license->getVersionId(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license status
|
||||
*/
|
||||
public function updateLicenseStatus(int $licenseId, string $status): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['status' => $status],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update license domain
|
||||
*/
|
||||
public function updateLicenseDomain(int $licenseId, string $domain): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['domain' => $this->normalizeDomain($domain)],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a license
|
||||
*/
|
||||
public function deleteLicense(int $licenseId): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->delete(
|
||||
$tableName,
|
||||
['id' => $licenseId],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize domain name
|
||||
*/
|
||||
public function normalizeDomain(string $domain): string
|
||||
{
|
||||
// Remove protocol
|
||||
$domain = preg_replace('#^https?://#', '', $domain);
|
||||
|
||||
// Remove trailing slash and path
|
||||
$domain = preg_replace('#/.*$#', '', $domain);
|
||||
|
||||
// Remove www prefix
|
||||
$domain = preg_replace('#^www\.#', '', $domain);
|
||||
|
||||
// Lowercase
|
||||
return strtolower(trim($domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version ID for a product
|
||||
*/
|
||||
private function getCurrentVersionId(int $productId): ?int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getVersionsTable();
|
||||
$versionId = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$tableName} WHERE product_id = %d AND is_active = 1 ORDER BY released_at DESC LIMIT 1",
|
||||
$productId
|
||||
)
|
||||
);
|
||||
|
||||
return $versionId ? (int) $versionId : null;
|
||||
}
|
||||
}
|
||||
163
src/Plugin.php
Normal file
163
src/Plugin.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Main Plugin class
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
||||
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
||||
use Jeremias\WcLicensedProduct\Frontend\AccountController;
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
public static function instance(): Plugin
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
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' => WP_DEBUG,
|
||||
]);
|
||||
|
||||
// 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();
|
||||
|
||||
// Initialize controllers
|
||||
new LicensedProductType();
|
||||
new CheckoutController($this->licenseManager);
|
||||
new AccountController($this->twig, $this->licenseManager);
|
||||
new RestApiController($this->licenseManager);
|
||||
|
||||
if (is_admin()) {
|
||||
new AdminController($this->twig, $this->licenseManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register plugin hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
// Generate license on order completion
|
||||
add_action('woocommerce_order_status_completed', [$this, 'onOrderCompleted']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle order completion - generate licenses
|
||||
*/
|
||||
public function onOrderCompleted(int $orderId): void
|
||||
{
|
||||
$order = wc_get_order($orderId);
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
if ($domain) {
|
||||
$this->licenseManager->generateLicense(
|
||||
$orderId,
|
||||
$product->get_id(),
|
||||
$order->get_customer_id(),
|
||||
$domain
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
103
src/Product/LicensedProduct.php
Normal file
103
src/Product/LicensedProduct.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Licensed Product Class
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Product
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Product;
|
||||
|
||||
use WC_Product;
|
||||
|
||||
/**
|
||||
* Licensed Product type extending WooCommerce Product
|
||||
*/
|
||||
class LicensedProduct extends WC_Product
|
||||
{
|
||||
/**
|
||||
* Product type
|
||||
*/
|
||||
protected $product_type = 'licensed';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($product = 0)
|
||||
{
|
||||
parent::__construct($product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type
|
||||
*/
|
||||
public function get_type(): string
|
||||
{
|
||||
return 'licensed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Licensed products are always virtual
|
||||
*/
|
||||
public function is_virtual(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Licensed products are purchasable
|
||||
*/
|
||||
public function is_purchasable(): bool
|
||||
{
|
||||
return $this->exists() && $this->get_price() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max activations for this product
|
||||
*/
|
||||
public function get_max_activations(): int
|
||||
{
|
||||
$value = $this->get_meta('_licensed_max_activations', true);
|
||||
return $value ? (int) $value : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validity days
|
||||
*/
|
||||
public function get_validity_days(): ?int
|
||||
{
|
||||
$value = $this->get_meta('_licensed_validity_days', true);
|
||||
return $value !== '' ? (int) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license should be bound to major version
|
||||
*/
|
||||
public function is_bound_to_version(): bool
|
||||
{
|
||||
return $this->get_meta('_licensed_bind_to_version', true) === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current software version
|
||||
*/
|
||||
public function get_current_version(): string
|
||||
{
|
||||
return $this->get_meta('_licensed_current_version', true) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get major version number from version string
|
||||
*/
|
||||
public function get_major_version(): int
|
||||
{
|
||||
$version = $this->get_current_version();
|
||||
if (empty($version)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$parts = explode('.', $version);
|
||||
return (int) ($parts[0] ?? 1);
|
||||
}
|
||||
}
|
||||
200
src/Product/LicensedProductType.php
Normal file
200
src/Product/LicensedProductType.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* Licensed Product Type
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Product
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Product;
|
||||
|
||||
/**
|
||||
* Registers and handles the Licensed product type for WooCommerce
|
||||
*/
|
||||
final class LicensedProductType
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
// Register product type
|
||||
add_filter('product_type_selector', [$this, 'addProductType']);
|
||||
add_filter('woocommerce_product_class', [$this, 'getProductClass'], 10, 2);
|
||||
|
||||
// Add product data tabs
|
||||
add_filter('woocommerce_product_data_tabs', [$this, 'addProductDataTab']);
|
||||
add_action('woocommerce_product_data_panels', [$this, 'addProductDataPanel']);
|
||||
|
||||
// Save product meta
|
||||
add_action('woocommerce_process_product_meta_licensed', [$this, 'saveProductMeta']);
|
||||
|
||||
// Show price and add to cart for licensed products
|
||||
add_action('woocommerce_licensed_add_to_cart', [$this, 'addToCartTemplate']);
|
||||
|
||||
// Make product virtual by default
|
||||
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product type to selector
|
||||
*/
|
||||
public function addProductType(array $types): array
|
||||
{
|
||||
$types['licensed'] = __('Licensed Product', 'wc-licensed-product');
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product class for licensed type
|
||||
*/
|
||||
public function getProductClass(string $className, string $productType): string
|
||||
{
|
||||
if ($productType === 'licensed') {
|
||||
return LicensedProduct::class;
|
||||
}
|
||||
return $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data tab for license settings
|
||||
*/
|
||||
public function addProductDataTab(array $tabs): array
|
||||
{
|
||||
$tabs['licensed_product'] = [
|
||||
'label' => __('License Settings', 'wc-licensed-product'),
|
||||
'target' => 'licensed_product_data',
|
||||
'class' => ['show_if_licensed'],
|
||||
'priority' => 21,
|
||||
];
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data panel content
|
||||
*/
|
||||
public function addProductDataPanel(): void
|
||||
{
|
||||
global $post;
|
||||
?>
|
||||
<div id="licensed_product_data" class="panel woocommerce_options_panel hidden">
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_licensed_max_activations',
|
||||
'label' => __('Max Activations', 'wc-licensed-product'),
|
||||
'description' => __('Maximum number of domain activations per license. Default: 1', 'wc-licensed-product'),
|
||||
'desc_tip' => true,
|
||||
'type' => 'number',
|
||||
'custom_attributes' => [
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
],
|
||||
'value' => get_post_meta($post->ID, '_licensed_max_activations', true) ?: '1',
|
||||
]);
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_licensed_validity_days',
|
||||
'label' => __('License Validity (Days)', 'wc-licensed-product'),
|
||||
'description' => __('Number of days the license is valid. Leave empty for lifetime license.', 'wc-licensed-product'),
|
||||
'desc_tip' => true,
|
||||
'type' => 'number',
|
||||
'custom_attributes' => [
|
||||
'min' => '0',
|
||||
'step' => '1',
|
||||
],
|
||||
]);
|
||||
|
||||
woocommerce_wp_checkbox([
|
||||
'id' => '_licensed_bind_to_version',
|
||||
'label' => __('Bind to Major Version', 'wc-licensed-product'),
|
||||
'description' => __('If enabled, licenses are bound to the major version at purchase time.', 'wc-licensed-product'),
|
||||
]);
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_licensed_current_version',
|
||||
'label' => __('Current Version', 'wc-licensed-product'),
|
||||
'description' => __('Current software version (e.g., 1.0.0)', 'wc-licensed-product'),
|
||||
'desc_tip' => true,
|
||||
'placeholder' => '1.0.0',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
// Show/hide panels based on product type
|
||||
$('select#product-type').change(function() {
|
||||
if ($(this).val() === 'licensed') {
|
||||
$('.show_if_licensed').show();
|
||||
$('.general_options').show();
|
||||
$('.pricing').show();
|
||||
} else {
|
||||
$('.show_if_licensed').hide();
|
||||
}
|
||||
}).change();
|
||||
|
||||
// Show general tab for licensed products
|
||||
$('#product-type').on('change', function() {
|
||||
if ($(this).val() === 'licensed') {
|
||||
$('.general_tab').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product meta
|
||||
*/
|
||||
public function saveProductMeta(int $postId): void
|
||||
{
|
||||
// Verify nonce is handled by WooCommerce
|
||||
$maxActivations = isset($_POST['_licensed_max_activations'])
|
||||
? absint($_POST['_licensed_max_activations'])
|
||||
: 1;
|
||||
update_post_meta($postId, '_licensed_max_activations', $maxActivations);
|
||||
|
||||
$validityDays = isset($_POST['_licensed_validity_days']) && $_POST['_licensed_validity_days'] !== ''
|
||||
? absint($_POST['_licensed_validity_days'])
|
||||
: '';
|
||||
update_post_meta($postId, '_licensed_validity_days', $validityDays);
|
||||
|
||||
$bindToVersion = isset($_POST['_licensed_bind_to_version']) ? 'yes' : 'no';
|
||||
update_post_meta($postId, '_licensed_bind_to_version', $bindToVersion);
|
||||
|
||||
$currentVersion = isset($_POST['_licensed_current_version'])
|
||||
? sanitize_text_field($_POST['_licensed_current_version'])
|
||||
: '';
|
||||
update_post_meta($postId, '_licensed_current_version', $currentVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart template for licensed products
|
||||
*/
|
||||
public function addToCartTemplate(): void
|
||||
{
|
||||
wc_get_template('single-product/add-to-cart/simple.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make licensed products virtual by default
|
||||
*/
|
||||
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
|
||||
{
|
||||
if ($product->is_type('licensed')) {
|
||||
return true;
|
||||
}
|
||||
return $isVirtual;
|
||||
}
|
||||
}
|
||||
102
templates/admin/licenses.html.twig
Normal file
102
templates/admin/licenses.html.twig
Normal file
@@ -0,0 +1,102 @@
|
||||
<div class="wrap">
|
||||
<h1>{{ __('Licenses') }}</h1>
|
||||
|
||||
{% for notice in notices %}
|
||||
<div class="notice notice-{{ notice.type }} is-dismissible">
|
||||
<p>{{ esc_html(notice.message) }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<p class="description">
|
||||
{{ __('Total licenses:') }} <strong>{{ total_licenses }}</strong>
|
||||
</p>
|
||||
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('License Key') }}</th>
|
||||
<th>{{ __('Product') }}</th>
|
||||
<th>{{ __('Customer') }}</th>
|
||||
<th>{{ __('Domain') }}</th>
|
||||
<th>{{ __('Status') }}</th>
|
||||
<th>{{ __('Expires') }}</th>
|
||||
<th>{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if licenses is empty %}
|
||||
<tr>
|
||||
<td colspan="7">{{ __('No licenses found.') }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for item in licenses %}
|
||||
<tr>
|
||||
<td><code>{{ item.license.licenseKey }}</code></td>
|
||||
<td>
|
||||
{% if item.product_edit_url %}
|
||||
<a href="{{ esc_url(item.product_edit_url) }}">{{ esc_html(item.product_name) }}</a>
|
||||
{% else %}
|
||||
{{ esc_html(item.product_name) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ esc_html(item.customer_name) }}
|
||||
{% if item.customer_email %}
|
||||
<br><small>{{ esc_html(item.customer_email) }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ esc_html(item.license.domain) }}</td>
|
||||
<td>
|
||||
<span class="license-status license-status-{{ item.license.status }}">
|
||||
{{ item.license.status|capitalize }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.license.expiresAt %}
|
||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||
{% else %}
|
||||
{{ __('Never') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.license.status != 'revoked' %}
|
||||
<a href="{{ admin_url ~ '?page=wc-licenses&action=revoke&license_id=' ~ item.license.id ~ '&_wpnonce=' }}"
|
||||
class="button button-small"
|
||||
onclick="return confirm('{{ __('Are you sure?') }}')">
|
||||
{{ __('Revoke') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ admin_url ~ '?page=wc-licenses&action=delete&license_id=' ~ item.license.id ~ '&_wpnonce=' }}"
|
||||
class="button button-small button-link-delete"
|
||||
onclick="return confirm('{{ __('Are you sure you want to delete this license?') }}')">
|
||||
{{ __('Delete') }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if total_pages > 1 %}
|
||||
<div class="tablenav bottom">
|
||||
<div class="tablenav-pages">
|
||||
<span class="pagination-links">
|
||||
{% if current_page > 1 %}
|
||||
<a class="prev-page button" href="{{ admin_url ~ '&paged=' ~ (current_page - 1) }}">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<span class="paging-input">
|
||||
{{ current_page }} {{ __('of') }} {{ total_pages }}
|
||||
</span>
|
||||
{% if current_page < total_pages %}
|
||||
<a class="next-page button" href="{{ admin_url ~ '&paged=' ~ (current_page + 1) }}">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
46
templates/frontend/licenses.html.twig
Normal file
46
templates/frontend/licenses.html.twig
Normal file
@@ -0,0 +1,46 @@
|
||||
{% if not has_licenses %}
|
||||
<p>{{ __('You have no licenses yet.') }}</p>
|
||||
{% else %}
|
||||
<table class="woocommerce-licenses-table shop_table shop_table_responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('License Key') }}</th>
|
||||
<th>{{ __('Product') }}</th>
|
||||
<th>{{ __('Domain') }}</th>
|
||||
<th>{{ __('Status') }}</th>
|
||||
<th>{{ __('Expires') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in licenses %}
|
||||
<tr>
|
||||
<td data-title="{{ __('License Key') }}">
|
||||
<code>{{ item.license.licenseKey }}</code>
|
||||
</td>
|
||||
<td data-title="{{ __('Product') }}">
|
||||
{% if item.product_url %}
|
||||
<a href="{{ esc_url(item.product_url) }}">{{ esc_html(item.product_name) }}</a>
|
||||
{% else %}
|
||||
{{ esc_html(item.product_name) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-title="{{ __('Domain') }}">
|
||||
{{ esc_html(item.license.domain) }}
|
||||
</td>
|
||||
<td data-title="{{ __('Status') }}">
|
||||
<span class="license-status license-status-{{ item.license.status }}">
|
||||
{{ item.license.status|capitalize }}
|
||||
</span>
|
||||
</td>
|
||||
<td data-title="{{ __('Expires') }}">
|
||||
{% if item.license.expiresAt %}
|
||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||
{% else %}
|
||||
{{ __('Never') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
113
wc-licensed-product.php
Normal file
113
wc-licensed-product.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WC Licensed Product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||
* Version: 0.0.1
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL-2.0-or-later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: wc-licensed-product
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.3
|
||||
* WC requires at least: 10.0
|
||||
* WC tested up to: 10.0
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct;
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.0.1');
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
// Load Composer autoloader
|
||||
if (file_exists(WC_LICENSED_PRODUCT_PLUGIN_DIR . 'vendor/autoload.php')) {
|
||||
require_once WC_LICENSED_PRODUCT_PLUGIN_DIR . 'vendor/autoload.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active
|
||||
*/
|
||||
function wc_licensed_product_is_woocommerce_active(): bool
|
||||
{
|
||||
return class_exists('WooCommerce');
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin notice for missing WooCommerce
|
||||
*/
|
||||
function wc_licensed_product_woocommerce_missing_notice(): void
|
||||
{
|
||||
?>
|
||||
<div class="error">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: WooCommerce plugin name */
|
||||
esc_html__('%s requires WooCommerce to be installed and active.', 'wc-licensed-product'),
|
||||
'<strong>WC Licensed Product</strong>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
function wc_licensed_product_init(): void
|
||||
{
|
||||
// Check for WooCommerce
|
||||
if (!wc_licensed_product_is_woocommerce_active()) {
|
||||
add_action('admin_notices', __NAMESPACE__ . '\\wc_licensed_product_woocommerce_missing_notice');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load text domain
|
||||
load_plugin_textdomain(
|
||||
'wc-licensed-product',
|
||||
false,
|
||||
dirname(WC_LICENSED_PRODUCT_PLUGIN_BASENAME) . '/languages'
|
||||
);
|
||||
|
||||
// Initialize the plugin
|
||||
Plugin::instance();
|
||||
}
|
||||
|
||||
// Hook into plugins_loaded to ensure WooCommerce is loaded first
|
||||
add_action('plugins_loaded', __NAMESPACE__ . '\\wc_licensed_product_init');
|
||||
|
||||
// Register activation hook
|
||||
register_activation_hook(__FILE__, function (): void {
|
||||
if (!wc_licensed_product_is_woocommerce_active()) {
|
||||
deactivate_plugins(WC_LICENSED_PRODUCT_PLUGIN_BASENAME);
|
||||
wp_die(
|
||||
esc_html__('WC Licensed Product requires WooCommerce to be installed and active.', 'wc-licensed-product'),
|
||||
'Plugin Activation Error',
|
||||
['back_link' => true]
|
||||
);
|
||||
}
|
||||
|
||||
// Run installation
|
||||
Installer::activate();
|
||||
});
|
||||
|
||||
// Register deactivation hook
|
||||
register_deactivation_hook(__FILE__, function (): void {
|
||||
Installer::deactivate();
|
||||
});
|
||||
Reference in New Issue
Block a user