You've already forked wc-licensed-product
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a90e6b18b | |||
| 502a8c7cd7 | |||
| 6b83fce8b2 | |||
| 8c33eaff29 | |||
| 98002ae3d7 | |||
| a93381dce6 | |||
| a522455a0a | |||
| 2de6abe133 | |||
| 8d60758f23 | |||
| 82bec621c6 | |||
| 034593f896 | |||
| 202f8a6dc0 | |||
| 36b51c9fc8 | |||
| d0aaf3180f | |||
| 4e683e2ff4 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.9] - 2026-01-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- "Generate Licenses" button in order meta box for admin-created orders
|
||||||
|
- "Generate Missing Licenses" button when some products in an order are missing licenses
|
||||||
|
- AJAX handler `ajaxGenerateOrderLicenses()` for manual license generation from admin
|
||||||
|
- Warning message when order domain is not set before generating licenses
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Critical:** Licenses are now generated for orders created manually in admin area
|
||||||
|
- Previously, licenses were only generated via checkout hooks, leaving admin-created orders without licenses
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Added `wclp_generate_order_licenses` AJAX action to `OrderLicenseController`
|
||||||
|
- Updated `order-licenses.js` with generate button handler and page reload on success
|
||||||
|
- Added CSS styles for generate status messages
|
||||||
|
- Updated translations (365 strings)
|
||||||
|
|
||||||
|
## [0.3.8] - 2026-01-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed duplicate German translation string causing `ArgumentCountError` in settings page
|
||||||
|
- The notification settings description had duplicated text with two `%s` placeholders
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `magdev/wc-licensed-product-client` to latest version (64d215c)
|
||||||
|
|
||||||
|
## [0.3.7] - 2026-01-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Download counter for licensed product versions (tracked per version)
|
||||||
|
- Download Statistics admin dashboard widget showing total downloads, top products, and top versions
|
||||||
|
- New `DownloadWidgetController` class for download statistics widget
|
||||||
|
- New `incrementDownloadCount()`, `getTotalDownloadCount()`, and `getDownloadStatistics()` methods in `VersionManager`
|
||||||
|
- New `download_count` column in product versions database table
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Dashboard widget "View All Licenses" link now uses correct page slug (`wc-licenses`)
|
||||||
|
- Download links in customer account page no longer result in 404 errors (added query var registration)
|
||||||
|
- Added `license-download` endpoint registration during plugin activation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed redundant "Status Breakdown" section from dashboard widget (info already shown in stat cards)
|
||||||
|
- License Types section in dashboard widget now uses card style matching the stats row above
|
||||||
|
- Improved dashboard widget visual consistency
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Added `addDownloadQueryVar()` method to `DownloadController` for proper endpoint registration
|
||||||
|
- Updated `Installer::activate()` to register `license-download` endpoint before flushing rewrite rules
|
||||||
|
- Updated translations (356 strings)
|
||||||
|
|
||||||
## [0.3.6] - 2026-01-23
|
## [0.3.6] - 2026-01-23
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
153
CLAUDE.md
153
CLAUDE.md
@@ -36,9 +36,13 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
No known bugs at the moment.
|
No known bugs at the moment.
|
||||||
|
|
||||||
|
### Version 0.3.9
|
||||||
|
|
||||||
|
No changes at the moment.
|
||||||
|
|
||||||
### Version 0.4.0
|
### Version 0.4.0
|
||||||
|
|
||||||
- On first plugin activation, get the checksums of all security related files (at least in `src/`) as hashes, store them encrypted on the server and add a mechanism to check the integrity of the files and the license validity periodically, control via wp-cron.
|
No changes at the moment.
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -974,3 +978,150 @@ Added admin dashboard widget for license statistics and automatic license expira
|
|||||||
- `autoExpireLicense()` updates status to expired and returns true if changed
|
- `autoExpireLicense()` updates status to expired and returns true if changed
|
||||||
- LicenseExpiredEmail follows same pattern as LicenseExpirationEmail (warning vs expired)
|
- LicenseExpiredEmail follows same pattern as LicenseExpirationEmail (warning vs expired)
|
||||||
- Expired notification tracked via user meta to prevent duplicate emails
|
- Expired notification tracked via user meta to prevent duplicate emails
|
||||||
|
|
||||||
|
### 2026-01-23 - Version 0.3.6 - Security Hardening
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Security audit and implementation alignment with client/server documentation. Fixed response signing compatibility, rate limiting security, and XSS prevention.
|
||||||
|
|
||||||
|
**Security Fixes:**
|
||||||
|
|
||||||
|
- Added CSRF protection (nonce verification) to CSV export functionality
|
||||||
|
- Fixed IP header spoofing vulnerability in rate limiting - now requires explicit trusted proxy configuration
|
||||||
|
- Enabled explicit Twig autoescape (`'html'`) for XSS protection
|
||||||
|
- Fixed unescaped status values in CSS class names in Twig templates
|
||||||
|
|
||||||
|
**Implementation Fixes:**
|
||||||
|
|
||||||
|
- Fixed response signing to use recursive key sorting for client library compatibility
|
||||||
|
- ResponseSigner now recursively sorts nested array keys alphabetically as required by `magdev/wc-licensed-product-client`
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Api/ResponseSigner.php` - Added `recursiveKeySort()` method for proper signature generation
|
||||||
|
- `src/Api/RestApiController.php` - Added trusted proxy support with `isTrustedProxy()`, `isCloudflareIp()`, `ipMatchesCidr()` methods
|
||||||
|
- `src/Plugin.php` - Added explicit `autoescape => 'html'` to Twig environment
|
||||||
|
- `src/Admin/AdminController.php` - Added nonce verification to `handleCsvExport()`, added `export_csv_url()` Twig function
|
||||||
|
- `templates/frontend/licenses.html.twig` - Added `esc_attr()` for CSS class status
|
||||||
|
- `templates/admin/licenses.html.twig` - Added `esc_attr()` for CSS class status, updated export link to use `export_csv_url()`
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
To enable trusted proxy support for rate limiting, add to `wp-config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// For Cloudflare
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE');
|
||||||
|
|
||||||
|
// Or for specific IPs/CIDR ranges
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.1,192.168.1.0/24');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Rate limiting now only trusts proxy headers (`HTTP_CF_CONNECTING_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_X_REAL_IP`) when `WC_LICENSE_TRUSTED_PROXIES` constant is defined
|
||||||
|
- Without trusted proxy configuration, rate limiting uses `REMOTE_ADDR` only (prevents IP spoofing)
|
||||||
|
- Cloudflare IP ranges are hardcoded for convenience (as of 2024)
|
||||||
|
- CIDR notation supported for custom proxy ranges
|
||||||
|
- Recursive key sorting ensures signature compatibility with SecureLicenseClient
|
||||||
|
- References: <https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/raw/branch/main/docs/server-implementation.md>
|
||||||
|
|
||||||
|
**Release v0.3.6:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.6.zip` (818 KB)
|
||||||
|
- SHA256: `b0063f0312759f090e12faba83de730baf4114139d763e46fad2b781d4b38270`
|
||||||
|
- Tagged as `v0.3.6` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-24 - Version 0.3.7 - Dashboard Improvements & Download Counter
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed dashboard widget bugs, improved UI consistency, and added download tracking functionality with a new statistics widget.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Fixed: Dashboard widget "View All Licenses" link used wrong page slug (`wc-licensed-product-licenses` instead of `wc-licenses`)
|
||||||
|
- Fixed: Download links in customer account resulted in 404 errors due to missing query var registration
|
||||||
|
- Added `license-download` endpoint registration during plugin activation in `Installer::activate()`
|
||||||
|
- Added `addDownloadQueryVar()` method to `DownloadController` for proper WordPress endpoint recognition
|
||||||
|
|
||||||
|
**UI Improvements:**
|
||||||
|
|
||||||
|
- Removed redundant "Status Breakdown" section from license statistics widget (info already shown in stat cards above)
|
||||||
|
- Changed License Types section to use card-style layout matching the stats row above
|
||||||
|
- Cleaned up unused CSS for status badges
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
|
||||||
|
- Download counter for licensed product versions (tracked per version in database)
|
||||||
|
- New Download Statistics admin dashboard widget showing:
|
||||||
|
- Total downloads count
|
||||||
|
- Top 5 products by downloads
|
||||||
|
- Top 5 versions by downloads
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `src/Admin/DownloadWidgetController.php` - Dashboard widget for download statistics
|
||||||
|
|
||||||
|
**New methods in VersionManager:**
|
||||||
|
|
||||||
|
- `incrementDownloadCount()` - Atomically increment download count for a version
|
||||||
|
- `getTotalDownloadCount()` - Get total downloads across all versions
|
||||||
|
- `getDownloadStatistics()` - Get download stats grouped by product and version
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Installer.php` - Added `download_count` column to versions table, added `license-download` endpoint registration
|
||||||
|
- `src/Product/ProductVersion.php` - Added `downloadCount` property and `getDownloadCount()` method
|
||||||
|
- `src/Product/VersionManager.php` - Added download counting methods
|
||||||
|
- `src/Frontend/DownloadController.php` - Added query var registration, increment download count on file serve
|
||||||
|
- `src/Admin/DashboardWidgetController.php` - Fixed URL, removed Status Breakdown, changed License Types to cards
|
||||||
|
- `src/Plugin.php` - Added DownloadWidgetController instantiation
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Download count is incremented atomically using SQL `download_count = download_count + 1`
|
||||||
|
- Statistics queries use SQL aggregation with product name enrichment via `wc_get_product()`
|
||||||
|
- WordPress endpoints require both `add_rewrite_endpoint()` AND `query_vars` filter registration
|
||||||
|
- Existing installations need to flush rewrite rules (Settings > Permalinks > Save) or reactivate plugin
|
||||||
|
|
||||||
|
**Release v0.3.7:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.7.zip` (827 KB)
|
||||||
|
- SHA256: `e93b2ab06f6d43c2179167090e07eda5db6809df6e391baece4ceba321cf33f6`
|
||||||
|
- Tagged as `v0.3.7` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-24 - Version 0.3.8 - Translation Bug Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed a critical translation bug that caused the settings page to crash with an `ArgumentCountError`.
|
||||||
|
|
||||||
|
**Bug Fix:**
|
||||||
|
|
||||||
|
- Fixed: Duplicate German translation string in `wc-licensed-product-de_CH.po` causing `ArgumentCountError` in settings page
|
||||||
|
- Root cause: The notification settings description was duplicated in the translation, resulting in two `%s` placeholders when only one argument was passed to `sprintf()`
|
||||||
|
- Location: [wc-licensed-product-de_CH.po:322-328](languages/wc-licensed-product-de_CH.po#L322-L328)
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `languages/wc-licensed-product-de_CH.po` - Removed duplicated translation string
|
||||||
|
- `languages/wc-licensed-product-de_CH.mo` - Recompiled binary translation
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Error was logged to `tmp/fatal-errors-2026-01-24.log`
|
||||||
|
- The German `msgstr` contained the same text twice, each with a `%s` placeholder
|
||||||
|
- `sprintf()` at `SettingsController.php:221` only provided one argument for the single `%s` in the English source
|
||||||
|
- Translation strings with `%s` placeholders must have exactly matching placeholder counts between source and translation
|
||||||
|
|
||||||
|
**Dependency Updates:**
|
||||||
|
|
||||||
|
- Updated `magdev/wc-licensed-product-client` from `9f513a8` to `64d215c`
|
||||||
|
|
||||||
|
**Release v0.3.8:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.8.zip` (829 KB)
|
||||||
|
- SHA256: `50ad6966c5ab8db2257572084d2d8a820448df62615678e1576696f2c0cb383d`
|
||||||
|
- Tagged as `v0.3.8` and pushed to `main` branch
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
// Order domain save
|
// Order domain save
|
||||||
$('#wclp-save-order-domain').on('click', this.saveOrderDomain.bind(this));
|
$('#wclp-save-order-domain').on('click', this.saveOrderDomain.bind(this));
|
||||||
|
|
||||||
|
// Generate licenses button
|
||||||
|
$(document).on('click', '#wclp-generate-licenses', this.generateLicenses.bind(this));
|
||||||
|
|
||||||
// License domain edit/save/cancel
|
// License domain edit/save/cancel
|
||||||
$(document).on('click', '.wclp-edit-domain-btn', this.startEditDomain);
|
$(document).on('click', '.wclp-edit-domain-btn', this.startEditDomain);
|
||||||
$(document).on('click', '.wclp-save-domain-btn', this.saveLicenseDomain.bind(this));
|
$(document).on('click', '.wclp-save-domain-btn', this.saveLicenseDomain.bind(this));
|
||||||
@@ -135,6 +138,54 @@
|
|||||||
$editBtn.show();
|
$editBtn.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate licenses for order
|
||||||
|
*/
|
||||||
|
generateLicenses: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(e.currentTarget);
|
||||||
|
var $spinner = $btn.siblings('.spinner');
|
||||||
|
var $status = $btn.siblings('.wclp-generate-status');
|
||||||
|
|
||||||
|
var orderId = $btn.data('order-id');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
$spinner.addClass('is-active');
|
||||||
|
$status.text('').removeClass('success error');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: wclpOrderLicenses.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wclp_generate_order_licenses',
|
||||||
|
nonce: wclpOrderLicenses.nonce,
|
||||||
|
order_id: orderId
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$status.text(response.data.message).addClass('success');
|
||||||
|
if (response.data.reload) {
|
||||||
|
// Reload the page after a short delay to show the new licenses
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$status.text(response.data.message || wclpOrderLicenses.strings.error).addClass('error');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$status.text(wclpOrderLicenses.strings.error).addClass('error');
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$spinner.removeClass('is-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save license domain
|
* Save license domain
|
||||||
*/
|
*/
|
||||||
|
|||||||
16
composer.lock
generated
16
composer.lock
generated
@@ -12,7 +12,7 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
||||||
"reference": "a3a957914fd6ef74cb479e213d1d3bc0606f496b"
|
"reference": "64d215cb265a64ff318cfbb954dd128b0076dc1d"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
||||||
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
||||||
},
|
},
|
||||||
"time": "2026-01-22T20:05:48+00:00"
|
"time": "2026-01-24T13:32:11+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
@@ -894,16 +894,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v3.22.2",
|
"version": "v3.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/twigphp/Twig.git",
|
"url": "https://github.com/twigphp/Twig.git",
|
||||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2"
|
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
"reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -957,7 +957,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/twigphp/Twig/issues",
|
"issues": "https://github.com/twigphp/Twig/issues",
|
||||||
"source": "https://github.com/twigphp/Twig/tree/v3.22.2"
|
"source": "https://github.com/twigphp/Twig/tree/v3.23.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -969,7 +969,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-14T11:28:47+00:00"
|
"time": "2026-01-23T21:00:41+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
releases/wc-licensed-product-0.3.7.zip
Normal file
BIN
releases/wc-licensed-product-0.3.7.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.3.7.zip.sha256
Normal file
1
releases/wc-licensed-product-0.3.7.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
e93b2ab06f6d43c2179167090e07eda5db6809df6e391baece4ceba321cf33f6 wc-licensed-product-0.3.7.zip
|
||||||
@@ -55,7 +55,7 @@ final class DashboardWidgetController
|
|||||||
public function renderWidget(): void
|
public function renderWidget(): void
|
||||||
{
|
{
|
||||||
$stats = $this->licenseManager->getStatistics();
|
$stats = $this->licenseManager->getStatistics();
|
||||||
$licensesUrl = admin_url('admin.php?page=wc-licensed-product-licenses');
|
$licensesUrl = admin_url('admin.php?page=wc-licenses');
|
||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.wclp-widget-stats {
|
.wclp-widget-stats {
|
||||||
@@ -96,40 +96,6 @@ final class DashboardWidgetController
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.wclp-widget-divider {
|
|
||||||
border-top: 1px solid #e2e4e7;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
.wclp-status-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.wclp-status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.wclp-status-badge.active {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
.wclp-status-badge.inactive {
|
|
||||||
background: #e2e3e5;
|
|
||||||
color: #383d41;
|
|
||||||
}
|
|
||||||
.wclp-status-badge.expired {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
.wclp-status-badge.revoked {
|
|
||||||
background: #d6d8db;
|
|
||||||
color: #1b1e21;
|
|
||||||
}
|
|
||||||
.wclp-widget-footer {
|
.wclp-widget-footer {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
@@ -160,61 +126,17 @@ final class DashboardWidgetController
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wclp-widget-divider"></div>
|
<div class="wclp-widget-stats">
|
||||||
|
<div class="wclp-stat-card">
|
||||||
<h4 style="margin: 0 0 8px 0; font-size: 13px; color: #1d2327;">
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['expiring'])); ?></div>
|
||||||
<?php esc_html_e('Status Breakdown', 'wc-licensed-product'); ?>
|
<div class="wclp-stat-label"><?php esc_html_e('Time-limited', 'wc-licensed-product'); ?></div>
|
||||||
</h4>
|
</div>
|
||||||
<div class="wclp-status-list">
|
<div class="wclp-stat-card">
|
||||||
<span class="wclp-status-badge active">
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['lifetime'])); ?></div>
|
||||||
<span class="dashicons dashicons-yes-alt" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
<div class="wclp-stat-label"><?php esc_html_e('Lifetime', 'wc-licensed-product'); ?></div>
|
||||||
<?php printf(
|
</div>
|
||||||
esc_html__('Active: %d', 'wc-licensed-product'),
|
|
||||||
$stats['by_status'][License::STATUS_ACTIVE]
|
|
||||||
); ?>
|
|
||||||
</span>
|
|
||||||
<span class="wclp-status-badge inactive">
|
|
||||||
<span class="dashicons dashicons-marker" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
|
||||||
<?php printf(
|
|
||||||
esc_html__('Inactive: %d', 'wc-licensed-product'),
|
|
||||||
$stats['by_status'][License::STATUS_INACTIVE]
|
|
||||||
); ?>
|
|
||||||
</span>
|
|
||||||
<span class="wclp-status-badge expired">
|
|
||||||
<span class="dashicons dashicons-clock" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
|
||||||
<?php printf(
|
|
||||||
esc_html__('Expired: %d', 'wc-licensed-product'),
|
|
||||||
$stats['by_status'][License::STATUS_EXPIRED]
|
|
||||||
); ?>
|
|
||||||
</span>
|
|
||||||
<span class="wclp-status-badge revoked">
|
|
||||||
<span class="dashicons dashicons-dismiss" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
|
||||||
<?php printf(
|
|
||||||
esc_html__('Revoked: %d', 'wc-licensed-product'),
|
|
||||||
$stats['by_status'][License::STATUS_REVOKED]
|
|
||||||
); ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wclp-widget-divider"></div>
|
|
||||||
|
|
||||||
<h4 style="margin: 0 0 8px 0; font-size: 13px; color: #1d2327;">
|
|
||||||
<?php esc_html_e('License Types', 'wc-licensed-product'); ?>
|
|
||||||
</h4>
|
|
||||||
<p style="margin: 0; font-size: 13px; color: #646970;">
|
|
||||||
<span class="dashicons dashicons-calendar-alt" style="font-size: 14px; width: 14px; height: 14px; vertical-align: text-bottom;"></span>
|
|
||||||
<?php printf(
|
|
||||||
esc_html__('Time-limited: %d', 'wc-licensed-product'),
|
|
||||||
$stats['expiring']
|
|
||||||
); ?>
|
|
||||||
|
|
|
||||||
<span class="dashicons dashicons-infinity" style="font-size: 14px; width: 14px; height: 14px; vertical-align: text-bottom;"></span>
|
|
||||||
<?php printf(
|
|
||||||
esc_html__('Lifetime: %d', 'wc-licensed-product'),
|
|
||||||
$stats['lifetime']
|
|
||||||
); ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="wclp-widget-footer">
|
<div class="wclp-widget-footer">
|
||||||
<a href="<?php echo esc_url($licensesUrl); ?>" class="button button-secondary">
|
<a href="<?php echo esc_url($licensesUrl); ?>" class="button button-secondary">
|
||||||
<?php esc_html_e('View All Licenses', 'wc-licensed-product'); ?>
|
<?php esc_html_e('View All Licenses', 'wc-licensed-product'); ?>
|
||||||
|
|||||||
184
src/Admin/DownloadWidgetController.php
Normal file
184
src/Admin/DownloadWidgetController.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Download Statistics Widget Controller
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Admin;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the WordPress admin dashboard widget for download statistics
|
||||||
|
*/
|
||||||
|
final class DownloadWidgetController
|
||||||
|
{
|
||||||
|
private VersionManager $versionManager;
|
||||||
|
|
||||||
|
public function __construct(VersionManager $versionManager)
|
||||||
|
{
|
||||||
|
$this->versionManager = $versionManager;
|
||||||
|
$this->registerHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register WordPress hooks
|
||||||
|
*/
|
||||||
|
private function registerHooks(): void
|
||||||
|
{
|
||||||
|
add_action('wp_dashboard_setup', [$this, 'registerDashboardWidget']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the dashboard widget
|
||||||
|
*/
|
||||||
|
public function registerDashboardWidget(): void
|
||||||
|
{
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_add_dashboard_widget(
|
||||||
|
'wclp_download_statistics',
|
||||||
|
__('Download Statistics', 'wc-licensed-product'),
|
||||||
|
[$this, 'renderWidget']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the dashboard widget content
|
||||||
|
*/
|
||||||
|
public function renderWidget(): void
|
||||||
|
{
|
||||||
|
$stats = $this->versionManager->getDownloadStatistics();
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
.wclp-download-widget-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.wclp-download-stat-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e2e4e7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 3px solid #2271b1;
|
||||||
|
}
|
||||||
|
.wclp-download-stat-number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.wclp-download-stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.wclp-download-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.wclp-download-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #e2e4e7;
|
||||||
|
}
|
||||||
|
.wclp-download-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.wclp-download-list .product-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d2327;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.wclp-download-list .version-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
.wclp-download-list .download-count {
|
||||||
|
background: #e7f5ff;
|
||||||
|
color: #0a4b78;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.wclp-download-section-title {
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.wclp-no-downloads {
|
||||||
|
color: #646970;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wclp-download-widget-stats">
|
||||||
|
<div class="wclp-download-stat-card">
|
||||||
|
<div class="wclp-download-stat-number"><?php echo esc_html(number_format_i18n($stats['total'])); ?></div>
|
||||||
|
<div class="wclp-download-stat-label"><?php esc_html_e('Total Downloads', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="wclp-download-section-title">
|
||||||
|
<?php esc_html_e('Top Products', 'wc-licensed-product'); ?>
|
||||||
|
</h4>
|
||||||
|
<?php if (!empty($stats['by_product'])): ?>
|
||||||
|
<ul class="wclp-download-list">
|
||||||
|
<?php foreach (array_slice($stats['by_product'], 0, 5) as $product): ?>
|
||||||
|
<li>
|
||||||
|
<span class="product-name"><?php echo esc_html($product['product_name']); ?></span>
|
||||||
|
<span class="download-count">
|
||||||
|
<?php echo esc_html(number_format_i18n($product['downloads'])); ?>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="wclp-no-downloads"><?php esc_html_e('No downloads yet', 'wc-licensed-product'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<h4 class="wclp-download-section-title">
|
||||||
|
<?php esc_html_e('Top Versions', 'wc-licensed-product'); ?>
|
||||||
|
</h4>
|
||||||
|
<?php if (!empty($stats['by_version'])): ?>
|
||||||
|
<ul class="wclp-download-list">
|
||||||
|
<?php foreach (array_slice($stats['by_version'], 0, 5) as $version): ?>
|
||||||
|
<li>
|
||||||
|
<span class="product-name">
|
||||||
|
<?php echo esc_html($version['product_name']); ?>
|
||||||
|
<span class="version-info">v<?php echo esc_html($version['version']); ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="download-count">
|
||||||
|
<?php echo esc_html(number_format_i18n($version['downloads'])); ?>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="wclp-no-downloads"><?php esc_html_e('No downloads yet', 'wc-licensed-product'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ final class OrderLicenseController
|
|||||||
// Handle AJAX actions
|
// Handle AJAX actions
|
||||||
add_action('wp_ajax_wclp_update_order_domain', [$this, 'ajaxUpdateOrderDomain']);
|
add_action('wp_ajax_wclp_update_order_domain', [$this, 'ajaxUpdateOrderDomain']);
|
||||||
add_action('wp_ajax_wclp_update_license_domain', [$this, 'ajaxUpdateLicenseDomain']);
|
add_action('wp_ajax_wclp_update_license_domain', [$this, 'ajaxUpdateLicenseDomain']);
|
||||||
|
add_action('wp_ajax_wclp_generate_order_licenses', [$this, 'ajaxGenerateOrderLicenses']);
|
||||||
|
|
||||||
// Enqueue admin scripts
|
// Enqueue admin scripts
|
||||||
add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
|
||||||
@@ -126,6 +127,18 @@ final class OrderLicenseController
|
|||||||
|
|
||||||
<h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4>
|
<h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Count licensed products to check if all have licenses
|
||||||
|
$licensedProductCount = 0;
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
if ($product && $product->is_type('licensed')) {
|
||||||
|
$licensedProductCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$missingLicenses = $licensedProductCount - count($licenses);
|
||||||
|
?>
|
||||||
|
|
||||||
<?php if (empty($licenses)): ?>
|
<?php if (empty($licenses)): ?>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php esc_html_e('No licenses have been generated for this order yet.', 'wc-licensed-product'); ?>
|
<?php esc_html_e('No licenses have been generated for this order yet.', 'wc-licensed-product'); ?>
|
||||||
@@ -137,6 +150,20 @@ final class OrderLicenseController
|
|||||||
<em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em>
|
<em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
|
<?php if ($orderDomain && $order->is_paid()): ?>
|
||||||
|
<p style="margin-top: 10px;">
|
||||||
|
<button type="button" class="button button-primary" id="wclp-generate-licenses" data-order-id="<?php echo esc_attr($order->get_id()); ?>">
|
||||||
|
<?php esc_html_e('Generate Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</button>
|
||||||
|
<span class="spinner" style="float: none; margin-top: 4px;"></span>
|
||||||
|
<span class="wclp-generate-status"></span>
|
||||||
|
</p>
|
||||||
|
<?php elseif (!$orderDomain): ?>
|
||||||
|
<p class="description" style="margin-top: 10px; color: #d63638;">
|
||||||
|
<span class="dashicons dashicons-warning"></span>
|
||||||
|
<?php esc_html_e('Please set the order domain above before generating licenses.', 'wc-licensed-product'); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<table class="widefat striped wclp-licenses-table">
|
<table class="widefat striped wclp-licenses-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -223,6 +250,29 @@ final class OrderLicenseController
|
|||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<?php if ($missingLicenses > 0 && $orderDomain && $order->is_paid()): ?>
|
||||||
|
<p style="margin-top: 10px;">
|
||||||
|
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %d: Number of missing licenses */
|
||||||
|
esc_html(_n(
|
||||||
|
'%d licensed product is missing a license.',
|
||||||
|
'%d licensed products are missing licenses.',
|
||||||
|
$missingLicenses,
|
||||||
|
'wc-licensed-product'
|
||||||
|
)),
|
||||||
|
$missingLicenses
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<button type="button" class="button" id="wclp-generate-licenses" data-order-id="<?php echo esc_attr($order->get_id()); ?>">
|
||||||
|
<?php esc_html_e('Generate Missing Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</button>
|
||||||
|
<span class="spinner" style="float: none; margin-top: 4px;"></span>
|
||||||
|
<span class="wclp-generate-status"></span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -248,6 +298,9 @@ final class OrderLicenseController
|
|||||||
.wclp-lifetime { color: #0073aa; font-weight: 500; }
|
.wclp-lifetime { color: #0073aa; font-weight: 500; }
|
||||||
.wclp-edit-domain-btn { color: #0073aa; text-decoration: none; }
|
.wclp-edit-domain-btn { color: #0073aa; text-decoration: none; }
|
||||||
.wclp-edit-domain-btn .dashicons { font-size: 16px; width: 16px; height: 16px; }
|
.wclp-edit-domain-btn .dashicons { font-size: 16px; width: 16px; height: 16px; }
|
||||||
|
.wclp-generate-status { font-style: italic; margin-left: 8px; }
|
||||||
|
.wclp-generate-status.success { color: #46b450; }
|
||||||
|
.wclp-generate-status.error { color: #dc3232; }
|
||||||
</style>
|
</style>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -284,8 +337,9 @@ final class OrderLicenseController
|
|||||||
'strings' => [
|
'strings' => [
|
||||||
'saving' => __('Saving...', 'wc-licensed-product'),
|
'saving' => __('Saving...', 'wc-licensed-product'),
|
||||||
'saved' => __('Saved!', 'wc-licensed-product'),
|
'saved' => __('Saved!', 'wc-licensed-product'),
|
||||||
'error' => __('Error saving. Please try again.', 'wc-licensed-product'),
|
'error' => __('Error. Please try again.', 'wc-licensed-product'),
|
||||||
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
|
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
|
||||||
|
'generating' => __('Generating...', 'wc-licensed-product'),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -392,4 +446,96 @@ final class OrderLicenseController
|
|||||||
$pattern = '/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
|
$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);
|
return (bool) preg_match($pattern, $domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for generating order licenses
|
||||||
|
*/
|
||||||
|
public function ajaxGenerateOrderLicenses(): void
|
||||||
|
{
|
||||||
|
check_ajax_referer('wclp_order_license_actions', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderId = absint($_POST['order_id'] ?? 0);
|
||||||
|
|
||||||
|
if (!$orderId) {
|
||||||
|
wp_send_json_error(['message' => __('Invalid order ID.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = wc_get_order($orderId);
|
||||||
|
if (!$order) {
|
||||||
|
wp_send_json_error(['message' => __('Order not found.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if order is paid
|
||||||
|
if (!$order->is_paid()) {
|
||||||
|
wp_send_json_error(['message' => __('Order must be paid before licenses can be generated.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get domain
|
||||||
|
$domain = $order->get_meta('_licensed_product_domain');
|
||||||
|
if (empty($domain)) {
|
||||||
|
wp_send_json_error(['message' => __('Please set the order domain before generating licenses.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate licenses for each licensed product
|
||||||
|
$generated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
if ($product && $product->is_type('licensed')) {
|
||||||
|
$license = $this->licenseManager->generateLicense(
|
||||||
|
$orderId,
|
||||||
|
$product->get_id(),
|
||||||
|
$order->get_customer_id(),
|
||||||
|
$domain
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($license) {
|
||||||
|
// Check if this is a new license or existing
|
||||||
|
$existingLicenses = $this->licenseManager->getLicensesByOrder($orderId);
|
||||||
|
$isNew = true;
|
||||||
|
foreach ($existingLicenses as $existing) {
|
||||||
|
if ($existing->getProductId() === $product->get_id() && $existing->getId() !== $license->getId()) {
|
||||||
|
$isNew = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isNew) {
|
||||||
|
$generated++;
|
||||||
|
} else {
|
||||||
|
$skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($generated > 0) {
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: %d: Number of licenses generated */
|
||||||
|
_n(
|
||||||
|
'%d license generated successfully.',
|
||||||
|
'%d licenses generated successfully.',
|
||||||
|
$generated,
|
||||||
|
'wc-licensed-product'
|
||||||
|
),
|
||||||
|
$generated
|
||||||
|
),
|
||||||
|
'generated' => $generated,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'reload' => true,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => __('All licenses already exist for this order.', 'wc-licensed-product'),
|
||||||
|
'generated' => 0,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'reload' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ final class DownloadController
|
|||||||
// Add download endpoint
|
// Add download endpoint
|
||||||
add_action('init', [$this, 'addDownloadEndpoint']);
|
add_action('init', [$this, 'addDownloadEndpoint']);
|
||||||
|
|
||||||
|
// Register query var for the endpoint
|
||||||
|
add_filter('query_vars', [$this, 'addDownloadQueryVar']);
|
||||||
|
|
||||||
// Handle download requests
|
// Handle download requests
|
||||||
add_action('template_redirect', [$this, 'handleDownloadRequest']);
|
add_action('template_redirect', [$this, 'handleDownloadRequest']);
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,15 @@ final class DownloadController
|
|||||||
add_rewrite_endpoint('license-download', EP_ROOT | EP_PAGES);
|
add_rewrite_endpoint('license-download', EP_ROOT | EP_PAGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the download query var
|
||||||
|
*/
|
||||||
|
public function addDownloadQueryVar(array $vars): array
|
||||||
|
{
|
||||||
|
$vars[] = 'license-download';
|
||||||
|
return $vars;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle download request
|
* Handle download request
|
||||||
*/
|
*/
|
||||||
@@ -160,8 +172,12 @@ final class DownloadController
|
|||||||
$downloadUrl = $version->getDownloadUrl();
|
$downloadUrl = $version->getDownloadUrl();
|
||||||
|
|
||||||
if ($attachmentId) {
|
if ($attachmentId) {
|
||||||
|
// Increment download count before serving
|
||||||
|
$this->versionManager->incrementDownloadCount($versionId);
|
||||||
$this->serveAttachment($attachmentId, $version->getVersion());
|
$this->serveAttachment($attachmentId, $version->getVersion());
|
||||||
} elseif ($downloadUrl) {
|
} elseif ($downloadUrl) {
|
||||||
|
// Increment download count before redirect
|
||||||
|
$this->versionManager->incrementDownloadCount($versionId);
|
||||||
// Redirect to external URL
|
// Redirect to external URL
|
||||||
wp_redirect($downloadUrl);
|
wp_redirect($downloadUrl);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ final class Installer
|
|||||||
// Set version in options
|
// Set version in options
|
||||||
update_option('wc_licensed_product_version', WC_LICENSED_PRODUCT_VERSION);
|
update_option('wc_licensed_product_version', WC_LICENSED_PRODUCT_VERSION);
|
||||||
|
|
||||||
// Register the licenses endpoint before flushing rewrite rules
|
// Register endpoints before flushing rewrite rules
|
||||||
add_rewrite_endpoint('licenses', EP_ROOT | EP_PAGES);
|
add_rewrite_endpoint('licenses', EP_ROOT | EP_PAGES);
|
||||||
|
add_rewrite_endpoint('license-download', EP_ROOT | EP_PAGES);
|
||||||
|
|
||||||
// Flush rewrite rules for REST API and My Account endpoints
|
// Flush rewrite rules for REST API and My Account endpoints
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
@@ -103,6 +104,7 @@ final class Installer
|
|||||||
download_url VARCHAR(512) DEFAULT NULL,
|
download_url VARCHAR(512) DEFAULT NULL,
|
||||||
attachment_id BIGINT UNSIGNED DEFAULT NULL,
|
attachment_id BIGINT UNSIGNED DEFAULT NULL,
|
||||||
file_hash VARCHAR(64) DEFAULT NULL,
|
file_hash VARCHAR(64) DEFAULT NULL,
|
||||||
|
download_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
released_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
released_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct;
|
|||||||
|
|
||||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\DashboardWidgetController;
|
use Jeremias\WcLicensedProduct\Admin\DashboardWidgetController;
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\DownloadWidgetController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||||
@@ -154,6 +155,7 @@ final class Plugin
|
|||||||
new OrderLicenseController($this->licenseManager);
|
new OrderLicenseController($this->licenseManager);
|
||||||
new SettingsController();
|
new SettingsController();
|
||||||
new DashboardWidgetController($this->licenseManager);
|
new DashboardWidgetController($this->licenseManager);
|
||||||
|
new DownloadWidgetController($this->versionManager);
|
||||||
|
|
||||||
// Show admin notice if unlicensed and not on localhost
|
// Show admin notice if unlicensed and not on localhost
|
||||||
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
|
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ProductVersion
|
|||||||
private ?string $downloadUrl;
|
private ?string $downloadUrl;
|
||||||
private ?int $attachmentId;
|
private ?int $attachmentId;
|
||||||
private ?string $fileHash;
|
private ?string $fileHash;
|
||||||
|
private int $downloadCount;
|
||||||
private bool $isActive;
|
private bool $isActive;
|
||||||
private \DateTimeInterface $releasedAt;
|
private \DateTimeInterface $releasedAt;
|
||||||
private \DateTimeInterface $createdAt;
|
private \DateTimeInterface $createdAt;
|
||||||
@@ -44,6 +45,7 @@ class ProductVersion
|
|||||||
$version->downloadUrl = $data['download_url'] ?: null;
|
$version->downloadUrl = $data['download_url'] ?: null;
|
||||||
$version->attachmentId = !empty($data['attachment_id']) ? (int) $data['attachment_id'] : null;
|
$version->attachmentId = !empty($data['attachment_id']) ? (int) $data['attachment_id'] : null;
|
||||||
$version->fileHash = $data['file_hash'] ?? null;
|
$version->fileHash = $data['file_hash'] ?? null;
|
||||||
|
$version->downloadCount = (int) ($data['download_count'] ?? 0);
|
||||||
$version->isActive = (bool) $data['is_active'];
|
$version->isActive = (bool) $data['is_active'];
|
||||||
$version->releasedAt = new \DateTimeImmutable($data['released_at']);
|
$version->releasedAt = new \DateTimeImmutable($data['released_at']);
|
||||||
$version->createdAt = new \DateTimeImmutable($data['created_at']);
|
$version->createdAt = new \DateTimeImmutable($data['created_at']);
|
||||||
@@ -144,6 +146,11 @@ class ProductVersion
|
|||||||
return $this->fileHash;
|
return $this->fileHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDownloadCount(): int
|
||||||
|
{
|
||||||
|
return $this->downloadCount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the download URL from attachment
|
* Get the download URL from attachment
|
||||||
*/
|
*/
|
||||||
@@ -197,6 +204,7 @@ class ProductVersion
|
|||||||
'download_url' => $this->downloadUrl,
|
'download_url' => $this->downloadUrl,
|
||||||
'attachment_id' => $this->attachmentId,
|
'attachment_id' => $this->attachmentId,
|
||||||
'file_hash' => $this->fileHash,
|
'file_hash' => $this->fileHash,
|
||||||
|
'download_count' => $this->downloadCount,
|
||||||
'is_active' => $this->isActive,
|
'is_active' => $this->isActive,
|
||||||
'released_at' => $this->releasedAt->format('Y-m-d H:i:s'),
|
'released_at' => $this->releasedAt->format('Y-m-d H:i:s'),
|
||||||
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
|
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
|
||||||
|
|||||||
@@ -276,4 +276,98 @@ class VersionManager
|
|||||||
|
|
||||||
return (int) $count > 0;
|
return (int) $count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment download count for a version
|
||||||
|
*/
|
||||||
|
public function incrementDownloadCount(int $versionId): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getVersionsTable();
|
||||||
|
$result = $wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"UPDATE {$tableName} SET download_count = download_count + 1 WHERE id = %d",
|
||||||
|
$versionId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total download count across all versions
|
||||||
|
*/
|
||||||
|
public function getTotalDownloadCount(): int
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getVersionsTable();
|
||||||
|
$count = $wpdb->get_var("SELECT COALESCE(SUM(download_count), 0) FROM {$tableName}");
|
||||||
|
|
||||||
|
return (int) $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get download statistics per product
|
||||||
|
*/
|
||||||
|
public function getDownloadStatistics(): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getVersionsTable();
|
||||||
|
|
||||||
|
// Get total downloads
|
||||||
|
$totalDownloads = $this->getTotalDownloadCount();
|
||||||
|
|
||||||
|
// Get downloads per product (top 10)
|
||||||
|
$byProduct = $wpdb->get_results(
|
||||||
|
"SELECT product_id, SUM(download_count) as downloads
|
||||||
|
FROM {$tableName}
|
||||||
|
GROUP BY product_id
|
||||||
|
ORDER BY downloads DESC
|
||||||
|
LIMIT 10",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get downloads per version (top 10)
|
||||||
|
$byVersion = $wpdb->get_results(
|
||||||
|
"SELECT id, product_id, version, download_count
|
||||||
|
FROM {$tableName}
|
||||||
|
WHERE download_count > 0
|
||||||
|
ORDER BY download_count DESC
|
||||||
|
LIMIT 10",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enrich product data with names
|
||||||
|
$productsWithNames = [];
|
||||||
|
foreach ($byProduct ?: [] as $row) {
|
||||||
|
$product = wc_get_product((int) $row['product_id']);
|
||||||
|
$productsWithNames[] = [
|
||||||
|
'product_id' => (int) $row['product_id'],
|
||||||
|
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
||||||
|
'downloads' => (int) $row['downloads'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich version data with product names
|
||||||
|
$versionsWithNames = [];
|
||||||
|
foreach ($byVersion ?: [] as $row) {
|
||||||
|
$product = wc_get_product((int) $row['product_id']);
|
||||||
|
$versionsWithNames[] = [
|
||||||
|
'version_id' => (int) $row['id'],
|
||||||
|
'product_id' => (int) $row['product_id'],
|
||||||
|
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
||||||
|
'version' => $row['version'],
|
||||||
|
'downloads' => (int) $row['download_count'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $totalDownloads,
|
||||||
|
'by_product' => $productsWithNames,
|
||||||
|
'by_version' => $versionsWithNames,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Licensed Product
|
* Plugin Name: WooCommerce Licensed Product
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/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.
|
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||||
* Version: 0.3.6
|
* Version: 0.3.9
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('WC_LICENSED_PRODUCT_VERSION', '0.3.6');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.3.9');
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__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_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user