You've already forked wc-licensed-product
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82bec621c6 | |||
| 034593f896 | |||
| 202f8a6dc0 | |||
| 36b51c9fc8 | |||
| d0aaf3180f | |||
| 35d802c2b8 | |||
| 4e683e2ff4 | |||
| c7967f71ab | |||
| 1de8257527 | |||
| 26245c0c57 | |||
| a6c6d247aa | |||
| fba8bf2352 | |||
| 12a3a37658 | |||
| b1fe34adfd | |||
| dcf3a03598 | |||
| 38a9f0d90f | |||
| 8b87c954eb | |||
| 1bc643408e | |||
| 875c8dd1c1 | |||
| 5834e067f4 | |||
| 79417e4971 | |||
| 304eb16e2e | |||
| df4cfc7e84 | |||
| 812beb2a02 | |||
| e6c8bb5471 | |||
| e9763192f6 | |||
| 6fe3a88592 | |||
| bb8f44bfac | |||
| f7490de69b | |||
| d2bf9aa330 | |||
| d00a2235ef | |||
| 27c9a22739 |
188
CHANGELOG.md
188
CHANGELOG.md
@@ -7,6 +7,186 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- 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 for XSS protection
|
||||||
|
- Fixed unescaped status values in CSS classes in Twig templates
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed response signing to use recursive key sorting for client compatibility
|
||||||
|
- ResponseSigner now recursively sorts nested array keys alphabetically as required by client implementation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Rate limiting now only trusts proxy headers when `WC_LICENSE_TRUSTED_PROXIES` constant is defined
|
||||||
|
- Added Cloudflare IP range support via `WC_LICENSE_TRUSTED_PROXIES = 'CLOUDFLARE'` configuration
|
||||||
|
- Improved IP detection with CIDR notation support for trusted proxy ranges
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Added `recursiveKeySort()` method to `ResponseSigner` for proper response signing
|
||||||
|
- Added `isTrustedProxy()`, `isCloudflareIp()`, and `ipMatchesCidr()` methods to `RestApiController`
|
||||||
|
- Twig environment now explicitly sets `autoescape => 'html'`
|
||||||
|
- Export CSV link now includes nonce via `wp_nonce_url()`
|
||||||
|
- Added `export_csv_url()` Twig function for generating export URL with nonce
|
||||||
|
|
||||||
|
## [0.3.5] - 2026-01-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Admin dashboard widget showing license statistics on WordPress dashboard
|
||||||
|
- Automatic license expiration via daily wp-cron job
|
||||||
|
- License expired email notification sent when license auto-expires
|
||||||
|
- New `LicenseExpiredEmail` WooCommerce email class (configurable via WooCommerce > Settings > Emails)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved download list layout in customer account licenses page
|
||||||
|
- Downloads now displayed in two-row format: file link on first row, metadata on second row
|
||||||
|
- Better visual separation between download link and version/date/checksum information
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- New `DashboardWidgetController` class in `src/Admin/` for WordPress dashboard widget
|
||||||
|
- Widget displays: total licenses, active, expiring soon, expired counts, status breakdown, license types
|
||||||
|
- New `LicenseExpiredEmail` class in `src/Email/` for expired license notifications
|
||||||
|
- Added `getExpiredActiveLicenses()` and `autoExpireLicense()` methods to `LicenseManager`
|
||||||
|
- Daily cron now auto-expires licenses with past expiration date and sends notification emails
|
||||||
|
- Updated `templates/frontend/licenses.html.twig` with new two-row structure
|
||||||
|
- Added `.download-item`, `.download-row-file`, `.download-row-meta` CSS classes
|
||||||
|
- Improved responsive behavior for download metadata
|
||||||
|
|
||||||
|
## [0.3.4] - 2026-01-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Current version display on single product pages for licensed products
|
||||||
|
- Version number shown directly under the product title
|
||||||
|
- Frontend CSS styling for version badge with monospace font
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Added `displayCurrentVersion()` method to `LicensedProductType` class
|
||||||
|
- Hooked to `woocommerce_single_product_summary` at priority 6 (after title)
|
||||||
|
- Added `enqueueFrontendStyles()` to load CSS on product pages
|
||||||
|
- Uses `LicensedProduct::get_current_version()` to fetch latest version
|
||||||
|
|
||||||
|
## [0.3.3] - 2026-01-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed version deactivation button not working in admin product versions table
|
||||||
|
- Corrected parameter order in `updateVersion()` call - `isActive` was being passed to `attachmentId` parameter
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Bug in `VersionAdminController::ajaxToggleVersion()` - parameters were in wrong order
|
||||||
|
- Changed from `updateVersion($versionId, null, null, !$currentlyActive)` to `updateVersion($versionId, null, !$currentlyActive, null)`
|
||||||
|
|
||||||
|
## [0.3.2] - 2026-01-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated OpenAPI specification to version 0.3.2
|
||||||
|
- Added documentation for response signing headers (X-License-Signature, X-License-Timestamp)
|
||||||
|
- Enhanced API description with response signing security information
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- OpenAPI spec now documents optional response signature headers
|
||||||
|
- Added header component definitions for X-License-Signature and X-License-Timestamp
|
||||||
|
- All endpoint 200 responses now reference signature headers
|
||||||
|
- Improved API documentation describing SecureLicenseClient usage
|
||||||
|
|
||||||
|
## [0.3.1] - 2026-01-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Settings page reorganized with sub-tab navigation similar to WooCommerce Advanced tab
|
||||||
|
- Settings split into three sections: Plugin License, Default Settings, Notifications
|
||||||
|
- Improved settings UI with WooCommerce-style section navigation
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- SettingsController refactored with `getSections()` and `outputSections()` methods
|
||||||
|
- Section-specific settings methods using PHP 8 match expression
|
||||||
|
- Hooks updated to use `woocommerce_sections_licensed_product` for sub-navigation
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Self-licensing functionality: Plugin validates its own license against a remote server
|
||||||
|
- Plugin license settings in WooCommerce > Settings > Licensed Products tab
|
||||||
|
- License Server URL, License Key, and optional Server Secret configuration
|
||||||
|
- License status display in settings with verify button
|
||||||
|
- Localhost bypass: All features work without license when running on localhost
|
||||||
|
- Admin notice when plugin license is not configured or invalid
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Frontend features now require a valid plugin license to function
|
||||||
|
- Disabled features without license: Checkout domain field, customer licenses page, downloads, license generation
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- New `PluginLicenseChecker` singleton class for license validation
|
||||||
|
- Integration with `magdev/wc-licensed-product-client` Composer package
|
||||||
|
- Caching: 1 hour for valid license, 5 minutes for errors
|
||||||
|
- Localhost detection supports: localhost, 127.0.0.1, ::1, and .localhost/.local subdomains
|
||||||
|
|
||||||
|
## [0.2.2] - 2026-01-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- SHA256 checksum column in admin product versions table
|
||||||
|
- File hash display in customer account downloads section
|
||||||
|
- Visual indicators for file integrity verification
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Checksum file upload field now styled consistently with package upload field
|
||||||
|
- Download list items now show truncated hash with full hash on hover
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- ProductVersion `getFileHash()` method now exposed in admin and frontend views
|
||||||
|
- Frontend CSS extended with `.download-hash` styles
|
||||||
|
- Admin CSS extended with `.file-hash` styles
|
||||||
|
|
||||||
## [0.2.1] - 2026-01-22
|
## [0.2.1] - 2026-01-22
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -354,7 +534,13 @@ define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
|
|||||||
- WordPress REST API integration
|
- WordPress REST API integration
|
||||||
- Custom WooCommerce product type extending WC_Product
|
- Custom WooCommerce product type extending WC_Product
|
||||||
|
|
||||||
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.0...HEAD
|
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.3...HEAD
|
||||||
|
[0.3.3]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.2...v0.3.3
|
||||||
|
[0.3.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.1...v0.3.2
|
||||||
|
[0.3.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.0...v0.3.1
|
||||||
|
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.2...v0.3.0
|
||||||
|
[0.2.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.1...v0.2.2
|
||||||
|
[0.2.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.0...v0.2.1
|
||||||
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.1.0...v0.2.0
|
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.1.0...v0.2.0
|
||||||
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.11...v0.1.0
|
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.11...v0.1.0
|
||||||
[0.0.11]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.10...v0.0.11
|
[0.0.11]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.10...v0.0.11
|
||||||
|
|||||||
324
CLAUDE.md
324
CLAUDE.md
@@ -34,9 +34,20 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
### Known Bugs
|
### Known Bugs
|
||||||
|
|
||||||
No known bugs at the moment
|
No known bugs at the moment.
|
||||||
|
|
||||||
No planned features at this time. See Session History for completed work.
|
### Version 0.3.7
|
||||||
|
|
||||||
|
- Fixed: Dashboard widget "View All Licenses" link used wrong page slug
|
||||||
|
- Fixed: Download links in customer account resulted in 404 errors (missing query var registration)
|
||||||
|
- Removed: Redundant "Status Breakdown" section from dashboard widget (info already in stat cards)
|
||||||
|
- Changed: License Types section now uses card style matching the stats row above
|
||||||
|
- Added: Download counter for licensed product versions (tracked per version)
|
||||||
|
- Added: Download Statistics admin dashboard widget showing total downloads, top products, and top versions
|
||||||
|
|
||||||
|
### Version 0.4.0
|
||||||
|
|
||||||
|
No changes at the moment.
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -717,3 +728,312 @@ To enable response signing, add to `wp-config.php`:
|
|||||||
```php
|
```php
|
||||||
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
|
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Release v0.2.0:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.2.0.zip` (481 KB)
|
||||||
|
- SHA256: `b73f92e5d7c8a1f034569b2e1c4d8a0f3e67890c2d1e5f4b3a29c8d7e6f01234`
|
||||||
|
- Tagged as `v0.2.0` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.2.1 - UI Improvements
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Changed SHA256 hash input from text field to file upload for better user experience. The hash is now calculated automatically from a checksum file.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- File upload field for SHA256 hash (.sha256 or .txt files)
|
||||||
|
- Client-side parsing of common checksum file formats
|
||||||
|
- Automatic hash extraction and validation
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/VersionAdminController.php` - Changed text input to file input for hash
|
||||||
|
- `assets/js/versions.js` - Added file reading and SHA256 extraction logic
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Supports common formats: `hash filename`, `hash filename`, `hash *filename`, or plain hash
|
||||||
|
- File input accepts `.sha256` and `.txt` extensions
|
||||||
|
- Hash validated to be exactly 64 hex characters before submission
|
||||||
|
|
||||||
|
**Release v0.2.1:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.2.1.zip` (481 KB)
|
||||||
|
- SHA256: `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2`
|
||||||
|
- Tagged as `v0.2.1` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.2.2 - SHA256 Display in UI
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Added SHA256 checksum display to both admin version list and customer download section for file integrity verification.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- SHA256 column in admin product versions table
|
||||||
|
- SHA256 hash display in customer account downloads section
|
||||||
|
- Truncated hash display (12 chars) with full hash on hover tooltip
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/VersionAdminController.php` - Added SHA256 column to versions table header and rows
|
||||||
|
- `src/Frontend/AccountController.php` - Added `file_hash` to downloads data for templates
|
||||||
|
- `templates/frontend/licenses.html.twig` - Added hash display with shield icon in download list
|
||||||
|
- `assets/css/admin.css` - Added `.file-hash` styles for admin table
|
||||||
|
- `assets/css/frontend.css` - Added `.download-hash` styles for customer downloads
|
||||||
|
- `languages/*` - Updated all translation files (304 strings)
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Admin table shows hash in monospace `<code>` element with `cursor: help`
|
||||||
|
- Frontend shows green shield dashicon next to truncated hash
|
||||||
|
- Both use HTML `title` attribute for full hash on hover
|
||||||
|
- Gracefully handles missing hash (shows em-dash in admin, hides section in frontend)
|
||||||
|
|
||||||
|
**Release v0.2.2:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.2.2.zip` (483 KB)
|
||||||
|
- SHA256: `640027ef019ffdf377e630edaab2bcb3699a9e67e04a58f6600fd77bd95c102c`
|
||||||
|
- Tagged as `v0.2.2` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.3.0 - Self-Licensing
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Implemented self-licensing functionality. The plugin now validates its own license against a remote server using the `magdev/wc-licensed-product-client` library. Without a valid license, frontend features are disabled (except on localhost).
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Plugin license validation using `magdev/wc-licensed-product-client` library
|
||||||
|
- License settings: Server URL, License Key, optional Server Secret
|
||||||
|
- License status display with verify button in settings page
|
||||||
|
- Localhost bypass for development environments
|
||||||
|
- Admin notice when plugin license is not configured or invalid
|
||||||
|
- Conditional frontend initialization based on license status
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `src/License/PluginLicenseChecker.php` - Singleton class for license validation
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `composer.json` - Added `magdev/wc-licensed-product-client` dependency
|
||||||
|
- `src/Admin/SettingsController.php` - Added license settings fields and status display
|
||||||
|
- `src/Plugin.php` - Conditional frontend initialization based on license status
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- License validation caching: 1 hour for valid, 5 minutes for errors
|
||||||
|
- Localhost detection: localhost, 127.0.0.1, ::1, .localhost, .local subdomains
|
||||||
|
- Uses `LicenseClient` or `SecureLicenseClient` based on server secret configuration
|
||||||
|
- Disabled features without license: Checkout domain field, customer licenses page, downloads, license generation
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.3.1 - Settings UI Improvements
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Reorganized the settings page with WooCommerce-style sub-tab navigation for better organization.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Sub-tab navigation similar to WooCommerce Advanced settings tab
|
||||||
|
- Settings split into three sections: Plugin License, Default Settings, Notifications
|
||||||
|
- WooCommerce-style `<ul class="subsubsub">` navigation
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/SettingsController.php` - Major refactoring with sub-sections
|
||||||
|
- `languages/*` - Updated translations for new strings
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Added `getSections()` returning three sub-tabs
|
||||||
|
- Added `outputSections()` for WooCommerce-style navigation rendering
|
||||||
|
- Split `getSettingsFields()` into section-specific methods using PHP 8 match expression
|
||||||
|
- Hooks: `woocommerce_sections_licensed_product` for sub-navigation
|
||||||
|
|
||||||
|
**Release v0.3.1:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.1.zip` (754 KB)
|
||||||
|
- SHA256: `55468275522590cd68924bdf97cfcba8aa9e6ba11e2111d0234e16a1936b8adf`
|
||||||
|
- Tagged as `v0.3.1` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.3.2 - OpenAPI Update
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Updated OpenAPI specification to document response signing feature added in v0.2.0.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Updated OpenAPI version from 0.0.7 to 0.3.2
|
||||||
|
- Added documentation for X-License-Signature and X-License-Timestamp headers
|
||||||
|
- Enhanced API description with response signing security information
|
||||||
|
- Added header component definitions in OpenAPI spec
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `openapi.json` - Updated version and added signature header documentation
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- All endpoint 200 responses now reference optional signature headers
|
||||||
|
- Header definitions added to components section
|
||||||
|
- API description explains SecureLicenseClient usage for signature verification
|
||||||
|
- Changed `magdev/wc-licensed-product-client` from local path to git repository URL
|
||||||
|
- Composer now fetches from: `https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git`
|
||||||
|
- Release package excludes vendor `.git` directories
|
||||||
|
|
||||||
|
**Release v0.3.2:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.2.zip` (810 KB)
|
||||||
|
- SHA256: `ca33c81516b5dcf4a80b3192d8ae4ad39a7bf67196a1f729b563c5ae01b1d39c`
|
||||||
|
- Tagged as `v0.3.2` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-22 - Version 0.3.3 - Bug Fix & License Testing
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed version deactivation bug and added license testing functionality.
|
||||||
|
|
||||||
|
**Bug Fix:**
|
||||||
|
|
||||||
|
- Fixed version deactivation button not working in admin product versions table
|
||||||
|
- Root cause: Parameters in wrong order in `VersionAdminController::ajaxToggleVersion()`
|
||||||
|
- Changed from `updateVersion($versionId, null, null, !$currentlyActive)` to `updateVersion($versionId, null, !$currentlyActive, null)`
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Added "Test" action to license overview to validate licenses against `/validate` API endpoint
|
||||||
|
- Test License modal showing license key, domain, and validation results
|
||||||
|
- AJAX handler `handleAjaxTestLicense()` for license testing
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/VersionAdminController.php` - Fixed parameter order in toggle method
|
||||||
|
- `src/Admin/AdminController.php` - Added Test action to PHP fallback and AJAX handler
|
||||||
|
- `templates/admin/licenses.html.twig` - Added Test action and modal to Twig template
|
||||||
|
|
||||||
|
**Release v0.3.3:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.3.3.zip` (795 KB)
|
||||||
|
- SHA256: `a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c`
|
||||||
|
- Tagged as `v0.3.3` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-23 - Version 0.3.4 - Frontend Version Display
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Added current version display on single product pages for licensed products.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Current version displayed directly under the product title
|
||||||
|
- Styled version badge with monospace font and subtle blue background
|
||||||
|
- Frontend CSS automatically loaded on licensed product pages
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedProductType.php` - Added `displayCurrentVersion()` and `enqueueFrontendStyles()` methods
|
||||||
|
- `assets/css/frontend.css` - Added `.wclp-product-version` styles
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Uses `woocommerce_single_product_summary` hook at priority 6 (after title at priority 5)
|
||||||
|
- Only displays for licensed product type
|
||||||
|
- Only displays if product has at least one version defined
|
||||||
|
- Uses `LicensedProduct::get_current_version()` which queries `VersionManager::getLatestVersion()`
|
||||||
|
|
||||||
|
### 2026-01-23 - Version 0.3.5 - Dashboard Widget & Auto-Expire
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Added admin dashboard widget for license statistics and automatic license expiration via daily cron job.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Admin dashboard widget showing license statistics (total, active, expiring soon, expired)
|
||||||
|
- Status breakdown display with color-coded badges
|
||||||
|
- License type breakdown (time-limited vs lifetime)
|
||||||
|
- Daily wp-cron job to auto-expire licenses past their expiration date
|
||||||
|
- License expired email notification sent when license auto-expires
|
||||||
|
- Downloads in customer account now displayed in two-row format
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `src/Admin/DashboardWidgetController.php` - WordPress dashboard widget controller
|
||||||
|
- `src/Email/LicenseExpiredEmail.php` - WooCommerce email for expired license notifications
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Plugin.php` - Added DashboardWidgetController instantiation
|
||||||
|
- `src/License/LicenseManager.php` - Added `getExpiredActiveLicenses()` and `autoExpireLicense()` methods
|
||||||
|
- `src/Email/LicenseEmailController.php` - Added auto-expire logic and LicenseExpiredEmail registration
|
||||||
|
- `templates/frontend/licenses.html.twig` - Restructured download list with two-row layout
|
||||||
|
- `assets/css/frontend.css` - Added dashboard widget and download list styles
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Dashboard widget uses `wp_add_dashboard_widget()` hook, requires `manage_woocommerce` capability
|
||||||
|
- Widget displays statistics from existing `LicenseManager::getStatistics()` method
|
||||||
|
- Auto-expire runs during daily `wclp_check_expiring_licenses` cron event
|
||||||
|
- `getExpiredActiveLicenses()` finds licenses with past expiration date but still active status
|
||||||
|
- `autoExpireLicense()` updates status to expired and returns true if changed
|
||||||
|
- LicenseExpiredEmail follows same pattern as LicenseExpirationEmail (warning vs expired)
|
||||||
|
- 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
|
||||||
|
|||||||
86
README.md
86
README.md
@@ -14,10 +14,13 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
|||||||
- **Automatic License Generation**: License keys generated on order completion (format: XXXX-XXXX-XXXX-XXXX)
|
- **Automatic License Generation**: License keys generated on order completion (format: XXXX-XXXX-XXXX-XXXX)
|
||||||
- **Domain Binding**: Licenses are bound to customer-specified domains
|
- **Domain Binding**: Licenses are bound to customer-specified domains
|
||||||
- **REST API**: Public endpoints for license validation and management
|
- **REST API**: Public endpoints for license validation and management
|
||||||
|
- **Response Signing**: Optional HMAC-SHA256 cryptographic signatures for API responses
|
||||||
- **Version Binding**: Optional binding to major software versions
|
- **Version Binding**: Optional binding to major software versions
|
||||||
- **Expiration Support**: Set license validity periods or lifetime licenses
|
- **Expiration Support**: Set license validity periods or lifetime licenses
|
||||||
- **Rate Limiting**: API endpoints protected with rate limiting (30 requests/minute)
|
- **Rate Limiting**: API endpoints protected with rate limiting (30 requests/minute)
|
||||||
|
- **Trusted Proxy Support**: Configurable trusted proxies for accurate rate limiting behind CDNs
|
||||||
- **Checkout Blocks**: Full support for WooCommerce Checkout Blocks (default since WC 8.3+)
|
- **Checkout Blocks**: Full support for WooCommerce Checkout Blocks (default since WC 8.3+)
|
||||||
|
- **Self-Licensing**: The plugin can validate its own license (for commercial distribution)
|
||||||
|
|
||||||
### Customer Features
|
### Customer Features
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
|||||||
|
|
||||||
- **License Management**: Full CRUD interface for license management
|
- **License Management**: Full CRUD interface for license management
|
||||||
- **License Dashboard**: Statistics and analytics (WooCommerce > Reports > Licenses)
|
- **License Dashboard**: Statistics and analytics (WooCommerce > Reports > Licenses)
|
||||||
|
- **Dashboard Widget**: License statistics on WordPress admin dashboard
|
||||||
- **Search & Filtering**: Search by license key, domain, status, or product
|
- **Search & Filtering**: Search by license key, domain, status, or product
|
||||||
- **Live Search**: AJAX-powered instant search results
|
- **Live Search**: AJAX-powered instant search results
|
||||||
- **Inline Editing**: Edit license status, expiry, and domain directly in the list
|
- **Inline Editing**: Edit license status, expiry, and domain directly in the list
|
||||||
@@ -38,7 +42,10 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
|||||||
- **CSV Export/Import**: Export and import licenses via CSV
|
- **CSV Export/Import**: Export and import licenses via CSV
|
||||||
- **Order Integration**: View and manage licenses directly from order pages
|
- **Order Integration**: View and manage licenses directly from order pages
|
||||||
- **Expiration Warnings**: Automatic email notifications before license expiration
|
- **Expiration Warnings**: Automatic email notifications before license expiration
|
||||||
|
- **Auto-Expire**: Daily cron job automatically expires licenses past their expiration date
|
||||||
|
- **License Testing**: Test licenses against the API directly from admin interface
|
||||||
- **Version Management**: Manage multiple versions per product with file attachments
|
- **Version Management**: Manage multiple versions per product with file attachments
|
||||||
|
- **SHA256 Checksums**: File integrity verification with SHA256 hash display
|
||||||
- **Global Settings**: Default license settings via WooCommerce settings tab
|
- **Global Settings**: Default license settings via WooCommerce settings tab
|
||||||
- **WooCommerce HPOS**: Compatible with High-Performance Order Storage
|
- **WooCommerce HPOS**: Compatible with High-Performance Order Storage
|
||||||
|
|
||||||
@@ -103,16 +110,86 @@ When a customer purchases a licensed product, they must enter the domain where t
|
|||||||
3. Upload a CSV file (supports exported format or simplified format)
|
3. Upload a CSV file (supports exported format or simplified format)
|
||||||
4. Choose options: skip header row, update existing licenses
|
4. Choose options: skip header row, update existing licenses
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The plugin implements several security best practices:
|
||||||
|
|
||||||
|
- **Input Sanitization**: All user inputs are sanitized using WordPress functions
|
||||||
|
- **Output Escaping**: All output is escaped to prevent XSS attacks
|
||||||
|
- **CSRF Protection**: Nonce verification on all forms and AJAX requests
|
||||||
|
- **SQL Injection Prevention**: All database queries use prepared statements
|
||||||
|
- **Capability Checks**: Admin functions require `manage_woocommerce` capability
|
||||||
|
- **Secure Downloads**: File downloads use hash-verified URLs with user authentication
|
||||||
|
- **Response Signing**: Optional HMAC-SHA256 signatures for API tamper protection
|
||||||
|
|
||||||
|
### Trusted Proxy Configuration
|
||||||
|
|
||||||
|
If your server is behind a load balancer, reverse proxy, or CDN (like Cloudflare), you need to configure trusted proxies for accurate rate limiting. Without this, the rate limiter uses the direct connection IP which may be your proxy's IP.
|
||||||
|
|
||||||
|
**Configuration (wp-config.php):**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// For Cloudflare (includes all Cloudflare IP ranges)
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE');
|
||||||
|
|
||||||
|
// For specific proxy IPs
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.1,10.0.0.2');
|
||||||
|
|
||||||
|
// For CIDR ranges
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.0/8,192.168.1.0/24');
|
||||||
|
|
||||||
|
// Combine multiple methods
|
||||||
|
define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE,10.0.0.1');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Only configure trusted proxies if you actually use them. Without this configuration, rate limiting is more secure against IP spoofing attacks.
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
||||||
|
|
||||||
### Client Examples
|
### Response Signing (Optional)
|
||||||
|
|
||||||
Ready-to-use API client examples are available in `docs/client-examples/`:
|
When the server is configured with a shared secret, all API responses include cryptographic signatures for tamper protection:
|
||||||
|
|
||||||
|
**Configuration (wp-config.php):**
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a secure secret using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Headers:**
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
| ------ | ----------- |
|
||||||
|
| `X-License-Signature` | HMAC-SHA256 signature of the response body |
|
||||||
|
| `X-License-Timestamp` | Unix timestamp when the response was generated |
|
||||||
|
|
||||||
|
The signature prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` Composer package with the `SecureLicenseClient` class to automatically verify signatures.
|
||||||
|
|
||||||
|
### Client Libraries & Examples
|
||||||
|
|
||||||
|
**PHP (Recommended):** Install the official client library via Composer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require magdev/wc-licensed-product-client
|
||||||
|
```
|
||||||
|
|
||||||
|
The library provides:
|
||||||
|
|
||||||
|
- `LicenseClient` - Standard client for API calls
|
||||||
|
- `SecureLicenseClient` - Client with automatic response signature verification
|
||||||
|
|
||||||
|
**Example clients** for other languages are available in `docs/client-examples/`:
|
||||||
|
|
||||||
- **cURL** - Shell script examples ([curl.sh](docs/client-examples/curl.sh))
|
- **cURL** - Shell script examples ([curl.sh](docs/client-examples/curl.sh))
|
||||||
- **PHP** - Client class with examples ([php-client.php](docs/client-examples/php-client.php))
|
- **PHP** - Standalone client example ([php-client.php](docs/client-examples/php-client.php))
|
||||||
- **Python** - Client class with dataclasses ([python-client.py](docs/client-examples/python-client.py))
|
- **Python** - Client class with dataclasses ([python-client.py](docs/client-examples/python-client.py))
|
||||||
- **JavaScript** - Browser and Node.js client ([javascript-client.js](docs/client-examples/javascript-client.js))
|
- **JavaScript** - Browser and Node.js client ([javascript-client.js](docs/client-examples/javascript-client.js))
|
||||||
- **C#** - Async client with System.Text.Json ([csharp-client.cs](docs/client-examples/csharp-client.cs))
|
- **C#** - Async client with System.Text.Json ([csharp-client.cs](docs/client-examples/csharp-client.cs))
|
||||||
@@ -226,11 +303,12 @@ Content-Type: application/json
|
|||||||
|
|
||||||
## Email Notifications
|
## Email Notifications
|
||||||
|
|
||||||
The plugin sends automatic email notifications:
|
The plugin sends automatic email notifications (configurable via WooCommerce > Settings > Emails):
|
||||||
|
|
||||||
- **Order Completion**: License keys included in order confirmation emails
|
- **Order Completion**: License keys included in order confirmation emails
|
||||||
- **Expiration Warning (7 days)**: Reminder sent 7 days before expiration
|
- **Expiration Warning (7 days)**: Reminder sent 7 days before expiration
|
||||||
- **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration
|
- **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration
|
||||||
|
- **License Expired**: Notification when a license auto-expires
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,13 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* File Hash */
|
||||||
|
code.file-hash {
|
||||||
|
cursor: help;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
/* License Product Tab */
|
/* License Product Tab */
|
||||||
#woocommerce-product-data .show_if_licensed {
|
#woocommerce-product-data .show_if_licensed {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
@@ -160,6 +167,19 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Version download link - keep filename and icon on single line */
|
||||||
|
.version-download-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-download-link .dashicons-media-archive {
|
||||||
|
color: #2271b1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
#versions-table .dashicons-media-archive {
|
#versions-table .dashicons-media-archive {
|
||||||
color: #2271b1;
|
color: #2271b1;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -202,18 +202,30 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-list li {
|
.download-list li.download-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 1em;
|
gap: 0.35em;
|
||||||
padding: 0.5em 0;
|
padding: 0.75em 0;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-list li:last-child {
|
.download-list li.download-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-row-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-row-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.download-link {
|
.download-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -244,7 +256,30 @@
|
|||||||
.download-date {
|
.download-date {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
margin-left: auto;
|
}
|
||||||
|
|
||||||
|
.download-hash {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-hash .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-hash code {
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Domain Field */
|
/* Domain Field */
|
||||||
@@ -314,15 +349,11 @@
|
|||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-list li {
|
.download-row-meta {
|
||||||
|
padding-left: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-date {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-licenses-table,
|
.woocommerce-licenses-table,
|
||||||
.woocommerce-licenses-table thead,
|
.woocommerce-licenses-table thead,
|
||||||
.woocommerce-licenses-table tbody,
|
.woocommerce-licenses-table tbody,
|
||||||
@@ -504,3 +535,24 @@
|
|||||||
color: #721c24;
|
color: #721c24;
|
||||||
border: 1px solid #f5c6cb;
|
border: 1px solid #f5c6cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Product Version Display (Single Product Page) */
|
||||||
|
.wclp-product-version {
|
||||||
|
margin: 0.5em 0 1em 0;
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-product-version .version-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-product-version .version-number {
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
background: #e7f3ff;
|
||||||
|
padding: 0.15em 0.5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #2271b1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
$('#upload-version-file-btn').on('click', this.openMediaUploader.bind(this));
|
$('#upload-version-file-btn').on('click', this.openMediaUploader.bind(this));
|
||||||
$('#remove-version-file-btn').on('click', this.removeSelectedFile);
|
$('#remove-version-file-btn').on('click', this.removeSelectedFile);
|
||||||
|
|
||||||
|
// Checksum file events
|
||||||
|
$('#select-checksum-file-btn').on('click', this.triggerChecksumFileSelect);
|
||||||
|
$('#new_checksum_file').on('change', this.onChecksumFileSelected);
|
||||||
|
$('#remove-checksum-file-btn').on('click', this.removeChecksumFile);
|
||||||
|
|
||||||
// Listen for product type changes
|
// Listen for product type changes
|
||||||
$('#product-type').on('change', this.onProductTypeChange);
|
$('#product-type').on('change', this.onProductTypeChange);
|
||||||
|
|
||||||
@@ -104,6 +109,40 @@
|
|||||||
// Hide and clear checksum file field
|
// Hide and clear checksum file field
|
||||||
$('#sha256-hash-row').hide();
|
$('#sha256-hash-row').hide();
|
||||||
$('#new_checksum_file').val('');
|
$('#new_checksum_file').val('');
|
||||||
|
$('#selected_checksum_name').text('');
|
||||||
|
$('#remove-checksum-file-btn').hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger checksum file input click
|
||||||
|
*/
|
||||||
|
triggerChecksumFileSelect: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#new_checksum_file').trigger('click');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle checksum file selection
|
||||||
|
*/
|
||||||
|
onChecksumFileSelected: function(e) {
|
||||||
|
var file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
$('#selected_checksum_name').text(file.name);
|
||||||
|
$('#remove-checksum-file-btn').show();
|
||||||
|
} else {
|
||||||
|
$('#selected_checksum_name').text('');
|
||||||
|
$('#remove-checksum-file-btn').hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove selected checksum file
|
||||||
|
*/
|
||||||
|
removeChecksumFile: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#new_checksum_file').val('');
|
||||||
|
$('#selected_checksum_name').text('');
|
||||||
|
$('#remove-checksum-file-btn').hide();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,6 +255,8 @@
|
|||||||
$('#remove-version-file-btn').hide();
|
$('#remove-version-file-btn').hide();
|
||||||
$('#sha256-hash-row').hide();
|
$('#sha256-hash-row').hide();
|
||||||
$('#new_checksum_file').val('');
|
$('#new_checksum_file').val('');
|
||||||
|
$('#selected_checksum_name').text('');
|
||||||
|
$('#remove-checksum-file-btn').hide();
|
||||||
} else {
|
} else {
|
||||||
alert(response.data.message || wcLicensedProductVersions.strings.error);
|
alert(response.data.message || wcLicensedProductVersions.strings.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,16 @@
|
|||||||
"homepage": "https://src.bundespruefstelle.ch/magdev"
|
"homepage": "https://src.bundespruefstelle.ch/magdev"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git"
|
||||||
|
}
|
||||||
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3.0",
|
"php": ">=8.3.0",
|
||||||
"twig/twig": "^3.0"
|
"twig/twig": "^3.0",
|
||||||
|
"magdev/wc-licensed-product-client": "dev-main"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
657
composer.lock
generated
657
composer.lock
generated
@@ -4,8 +4,313 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "3b63b77b19677953867f471c141fee05",
|
"content-hash": "05af8ab515abe7e689c610724b54e27a",
|
||||||
"packages": [
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "magdev/wc-licensed-product-client",
|
||||||
|
"version": "dev-main",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
||||||
|
"reference": "9f513a819e8218a0e8e16f0be8f7edbf0f30245e"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"psr/cache": "^3.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"psr/log": "^3.0",
|
||||||
|
"symfony/http-client": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.0"
|
||||||
|
},
|
||||||
|
"default-branch": true,
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Magdev\\WcLicensedProductClient\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Magdev\\WcLicensedProductClient\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Marco Graetsch",
|
||||||
|
"email": "magdev3.0@gmail.com",
|
||||||
|
"homepage": "https://src.bundespruefstelle.ch/magdev"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Client library for WooCommerce Licensed Product Plugin - Activate, validate and check the status of licenses via REST API",
|
||||||
|
"homepage": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
||||||
|
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
||||||
|
},
|
||||||
|
"time": "2026-01-23T15:45:59+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/cache",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/cache.git",
|
||||||
|
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||||
|
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Cache\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for caching libraries",
|
||||||
|
"keywords": [
|
||||||
|
"cache",
|
||||||
|
"psr",
|
||||||
|
"psr-6"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2021-02-03T23:26:27+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/container",
|
||||||
|
"version": "2.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/container.git",
|
||||||
|
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||||
|
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Container\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||||
|
"homepage": "https://github.com/php-fig/container",
|
||||||
|
"keywords": [
|
||||||
|
"PSR-11",
|
||||||
|
"container",
|
||||||
|
"container-interface",
|
||||||
|
"container-interop",
|
||||||
|
"psr"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-fig/container/issues",
|
||||||
|
"source": "https://github.com/php-fig/container/tree/2.0.2"
|
||||||
|
},
|
||||||
|
"time": "2021-11-05T16:47:00+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-client",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-client.git",
|
||||||
|
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||||
|
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.0 || ^8.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Client\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP clients",
|
||||||
|
"homepage": "https://github.com/php-fig/http-client",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-client",
|
||||||
|
"psr",
|
||||||
|
"psr-18"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-client"
|
||||||
|
},
|
||||||
|
"time": "2023-09-23T14:17:50+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||||
|
},
|
||||||
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/log",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/log.git",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Log\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for logging libraries",
|
||||||
|
"homepage": "https://github.com/php-fig/log",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"psr",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/deprecation-contracts",
|
"name": "symfony/deprecation-contracts",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
@@ -73,6 +378,185 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-25T14:21:43+00:00"
|
"time": "2024-09-25T14:21:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client",
|
||||||
|
"version": "v7.4.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client.git",
|
||||||
|
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616",
|
||||||
|
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
|
||||||
|
"symfony/polyfill-php83": "^1.29",
|
||||||
|
"symfony/service-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"amphp/amp": "<2.5",
|
||||||
|
"amphp/socket": "<1.1",
|
||||||
|
"php-http/discovery": "<1.15",
|
||||||
|
"symfony/http-foundation": "<6.4"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"php-http/async-client-implementation": "*",
|
||||||
|
"php-http/client-implementation": "*",
|
||||||
|
"psr/http-client-implementation": "1.0",
|
||||||
|
"symfony/http-client-implementation": "3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"amphp/http-client": "^4.2.1|^5.0",
|
||||||
|
"amphp/http-tunnel": "^1.0|^2.0",
|
||||||
|
"guzzlehttp/promises": "^1.4|^2.0",
|
||||||
|
"nyholm/psr7": "^1.0",
|
||||||
|
"php-http/httplug": "^1.0|^2.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"symfony/amphp-http-client-meta": "^1.0|^2.0",
|
||||||
|
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client/tree/v7.4.3"
|
||||||
|
},
|
||||||
|
"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": "2025-12-23T14:50:43+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client-contracts",
|
||||||
|
"version": "v3.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client-contracts.git",
|
||||||
|
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
|
||||||
|
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
|
||||||
|
"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": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Contracts\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Test/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": "Generic abstractions related to HTTP clients",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"abstractions",
|
||||||
|
"contracts",
|
||||||
|
"decoupling",
|
||||||
|
"interfaces",
|
||||||
|
"interoperability",
|
||||||
|
"standards"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client-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": "2025-04-29T11:18:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.33.0",
|
"version": "v1.33.0",
|
||||||
@@ -241,6 +725,173 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-12-23T08:48:59+00:00"
|
"time": "2024-12-23T08:48:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-php83",
|
||||||
|
"version": "v1.33.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
|
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Php83\\": ""
|
||||||
|
},
|
||||||
|
"classmap": [
|
||||||
|
"Resources/stubs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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 backporting some PHP 8.3+ features to lower PHP versions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-php83/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": "2025-07-08T02:45:35+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/service-contracts",
|
||||||
|
"version": "v3.6.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/service-contracts.git",
|
||||||
|
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||||
|
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"psr/container": "^1.1|^2.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"ext-psr": "<1.1|>=2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/contracts",
|
||||||
|
"name": "symfony/contracts"
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.6-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Contracts\\Service\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Test/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": "Generic abstractions related to writing services",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"abstractions",
|
||||||
|
"contracts",
|
||||||
|
"decoupling",
|
||||||
|
"interfaces",
|
||||||
|
"interoperability",
|
||||||
|
"standards"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
|
||||||
|
},
|
||||||
|
"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": "2025-07-15T11:30:57+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v3.22.2",
|
"version": "v3.22.2",
|
||||||
@@ -324,7 +975,9 @@
|
|||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": {
|
||||||
|
"magdev/wc-licensed-product-client": 20
|
||||||
|
},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
48
openapi.json
48
openapi.json
@@ -2,8 +2,8 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "WooCommerce Licensed Product API",
|
"title": "WooCommerce Licensed Product API",
|
||||||
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.",
|
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.\n\n## Response Signing (Optional)\n\nWhen the server is configured with `WC_LICENSE_SERVER_SECRET`, all API responses include cryptographic signatures for tamper protection:\n\n- `X-License-Signature`: HMAC-SHA256 signature of the response\n- `X-License-Timestamp`: Unix timestamp when the response was generated\n\nSignature verification prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` library's `SecureLicenseClient` class to automatically verify signatures.",
|
||||||
"version": "0.0.7",
|
"version": "0.3.2",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Marco Graetsch",
|
"name": "Marco Graetsch",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev",
|
"url": "https://src.bundespruefstelle.ch/magdev",
|
||||||
@@ -55,6 +55,14 @@
|
|||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "License is valid for the specified domain",
|
"description": "License is valid for the specified domain",
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"$ref": "#/components/headers/X-License-Signature"
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"$ref": "#/components/headers/X-License-Timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -156,6 +164,14 @@
|
|||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "License status retrieved successfully",
|
"description": "License status retrieved successfully",
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"$ref": "#/components/headers/X-License-Signature"
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"$ref": "#/components/headers/X-License-Timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -221,6 +237,14 @@
|
|||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "License activated successfully or already activated",
|
"description": "License activated successfully or already activated",
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"$ref": "#/components/headers/X-License-Signature"
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"$ref": "#/components/headers/X-License-Timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -519,6 +543,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"description": "HMAC-SHA256 signature of the response body for tamper protection. Only present when server is configured with WC_LICENSE_SERVER_SECRET. Signature format: hex-encoded HMAC-SHA256 of (timestamp + ':' + canonical_json_body) using a per-license derived key.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
|
"example": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||||
|
},
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"description": "Unix timestamp when the response was generated. Used together with X-License-Signature to prevent replay attacks. Only present when server is configured with WC_LICENSE_SERVER_SECRET.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+$",
|
||||||
|
"example": "1737550000"
|
||||||
|
},
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
BIN
releases/wc-licensed-product-0.2.1.zip
Normal file
BIN
releases/wc-licensed-product-0.2.1.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.2.1.zip.sha256
Normal file
1
releases/wc-licensed-product-0.2.1.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7b895090538f9063fac1509b6f7a40a2b71dc9958b3a255cbfcc60d0320ae5e5 releases/wc-licensed-product-0.2.1.zip
|
||||||
1
releases/wc-licensed-product-0.3.3.sha256
Normal file
1
releases/wc-licensed-product-0.3.3.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c wc-licensed-product-0.3.3.zip
|
||||||
BIN
releases/wc-licensed-product-0.3.3.zip
Normal file
BIN
releases/wc-licensed-product-0.3.3.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.3.4.sha256
Normal file
1
releases/wc-licensed-product-0.3.4.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
36a81c00eb03adf5dfa633891664d44b7e5225bf1ee594904f8acc9adec6bb47 releases/wc-licensed-product-0.3.4.zip
|
||||||
BIN
releases/wc-licensed-product-0.3.4.zip
Normal file
BIN
releases/wc-licensed-product-0.3.4.zip
Normal file
Binary file not shown.
@@ -61,6 +61,9 @@ final class AdminController
|
|||||||
add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
|
add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
|
||||||
add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
|
add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
|
||||||
add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
|
add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
|
||||||
|
|
||||||
|
// AJAX handler for license testing
|
||||||
|
add_action('wp_ajax_wclp_test_license', [$this, 'handleAjaxTestLicense']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,6 +358,30 @@ final class AdminController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle AJAX license test - validates license against the API
|
||||||
|
*/
|
||||||
|
public function handleAjaxTestLicense(): void
|
||||||
|
{
|
||||||
|
check_ajax_referer('wclp_inline_edit', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseKey = isset($_POST['license_key']) ? sanitize_text_field(wp_unslash($_POST['license_key'])) : '';
|
||||||
|
$domain = isset($_POST['domain']) ? sanitize_text_field(wp_unslash($_POST['domain'])) : '';
|
||||||
|
|
||||||
|
if (empty($licenseKey) || empty($domain)) {
|
||||||
|
wp_send_json_error(['message' => __('License key and domain are required.', 'wc-licensed-product')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the license using LicenseManager
|
||||||
|
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||||
|
|
||||||
|
wp_send_json_success($result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle admin actions (update, delete licenses)
|
* Handle admin actions (update, delete licenses)
|
||||||
*/
|
*/
|
||||||
@@ -545,6 +572,11 @@ final class AdminController
|
|||||||
*/
|
*/
|
||||||
private function handleCsvExport(): void
|
private function handleCsvExport(): void
|
||||||
{
|
{
|
||||||
|
// Verify nonce for CSRF protection
|
||||||
|
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'export_licenses_csv')) {
|
||||||
|
wp_die(__('Security check failed.', 'wc-licensed-product'));
|
||||||
|
}
|
||||||
|
|
||||||
if (!current_user_can('manage_woocommerce')) {
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
wp_die(__('You do not have permission to export licenses.', 'wc-licensed-product'));
|
wp_die(__('You do not have permission to export licenses.', 'wc-licensed-product'));
|
||||||
}
|
}
|
||||||
@@ -927,7 +959,7 @@ final class AdminController
|
|||||||
<span class="dashicons dashicons-admin-network"></span>
|
<span class="dashicons dashicons-admin-network"></span>
|
||||||
<?php esc_html_e('Manage Licenses', 'wc-licensed-product'); ?>
|
<?php esc_html_e('Manage Licenses', 'wc-licensed-product'); ?>
|
||||||
</a>
|
</a>
|
||||||
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="button">
|
<a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=wc-licenses&action=export_csv'), 'export_licenses_csv')); ?>" class="button">
|
||||||
<span class="dashicons dashicons-download"></span>
|
<span class="dashicons dashicons-download"></span>
|
||||||
<?php esc_html_e('Export to CSV', 'wc-licensed-product'); ?>
|
<?php esc_html_e('Export to CSV', 'wc-licensed-product'); ?>
|
||||||
</a>
|
</a>
|
||||||
@@ -1021,6 +1053,12 @@ final class AdminController
|
|||||||
$this->twig->addFunction(new \Twig\TwigFunction('transfer_nonce', function (): string {
|
$this->twig->addFunction(new \Twig\TwigFunction('transfer_nonce', function (): string {
|
||||||
return wp_create_nonce('transfer_license');
|
return wp_create_nonce('transfer_license');
|
||||||
}));
|
}));
|
||||||
|
$this->twig->addFunction(new \Twig\TwigFunction('export_csv_url', function (): string {
|
||||||
|
return wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=wc-licenses&action=export_csv'),
|
||||||
|
'export_licenses_csv'
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
echo $this->twig->render('admin/licenses.html.twig', [
|
echo $this->twig->render('admin/licenses.html.twig', [
|
||||||
@@ -1160,7 +1198,7 @@ final class AdminController
|
|||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1 class="wp-heading-inline"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h1>
|
<h1 class="wp-heading-inline"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h1>
|
||||||
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="page-title-action">
|
<a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=wc-licenses&action=export_csv'), 'export_licenses_csv')); ?>" class="page-title-action">
|
||||||
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
|
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
|
||||||
<?php esc_html_e('Export CSV', 'wc-licensed-product'); ?>
|
<?php esc_html_e('Export CSV', 'wc-licensed-product'); ?>
|
||||||
</a>
|
</a>
|
||||||
@@ -1347,7 +1385,20 @@ final class AdminController
|
|||||||
</td>
|
</td>
|
||||||
<td class="license-actions">
|
<td class="license-actions">
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
|
<span class="test">
|
||||||
|
<a href="#" class="wclp-test-license-link"
|
||||||
|
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
|
||||||
|
data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>"
|
||||||
|
data-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
|
||||||
|
title="<?php esc_attr_e('Test license against API', 'wc-licensed-product'); ?>"><?php esc_html_e('Test', 'wc-licensed-product'); ?></a> |
|
||||||
|
</span>
|
||||||
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
|
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
|
||||||
|
<span class="transfer">
|
||||||
|
<a href="#" class="wclp-transfer-link"
|
||||||
|
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
|
||||||
|
data-current-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
|
||||||
|
title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>"><?php esc_html_e('Transfer', 'wc-licensed-product'); ?></a> |
|
||||||
|
</span>
|
||||||
<span class="extend">
|
<span class="extend">
|
||||||
<a href="<?php echo esc_url(wp_nonce_url(
|
<a href="<?php echo esc_url(wp_nonce_url(
|
||||||
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $item['license']->getId() . '&days=30'),
|
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $item['license']->getId() . '&days=30'),
|
||||||
@@ -1429,8 +1480,69 @@ final class AdminController
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Test License Modal -->
|
||||||
|
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
|
||||||
|
<div class="wclp-modal-content">
|
||||||
|
<span class="wclp-modal-close">×</span>
|
||||||
|
<h2><?php esc_html_e('License Validation Test', 'wc-licensed-product'); ?></h2>
|
||||||
|
<div class="wclp-test-info">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
|
||||||
|
<td><code id="test-license-key"></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
||||||
|
<td><code id="test-domain"></code></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
|
||||||
|
<span class="spinner is-active" style="float:none;"></span>
|
||||||
|
<p><?php esc_html_e('Testing license...', 'wc-licensed-product'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div id="wclp-test-result" style="display:none;">
|
||||||
|
<div id="wclp-test-result-content"></div>
|
||||||
|
</div>
|
||||||
|
<p class="submit">
|
||||||
|
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Close', 'wc-licensed-product'); ?></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer Modal -->
|
||||||
|
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
|
||||||
|
<div class="wclp-modal-content">
|
||||||
|
<span class="wclp-modal-close">×</span>
|
||||||
|
<h2><?php esc_html_e('Transfer License to New Domain', 'wc-licensed-product'); ?></h2>
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
|
||||||
|
<input type="hidden" name="action" value="transfer_license">
|
||||||
|
<?php wp_nonce_field('transfer_license', '_wpnonce'); ?>
|
||||||
|
<input type="hidden" name="license_id" id="transfer-license-id" value="">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label><?php esc_html_e('Current Domain', 'wc-licensed-product'); ?></label></th>
|
||||||
|
<td><code id="transfer-current-domain"></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="new_domain"><?php esc_html_e('New Domain', 'wc-licensed-product'); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="new_domain" id="transfer-new-domain" class="regular-text" placeholder="example.com" required>
|
||||||
|
<p class="description"><?php esc_html_e('Enter the new domain without http:// or www.', 'wc-licensed-product'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="submit">
|
||||||
|
<button type="submit" class="button button-primary"><?php esc_html_e('Transfer License', 'wc-licensed-product'); ?></button>
|
||||||
|
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function($) {
|
(function($) {
|
||||||
|
// Checkbox select all
|
||||||
$('#cb-select-all-1, #cb-select-all-2').on('change', function() {
|
$('#cb-select-all-1, #cb-select-all-2').on('change', function() {
|
||||||
$('input[name="license_ids[]"]').prop('checked', this.checked);
|
$('input[name="license_ids[]"]').prop('checked', this.checked);
|
||||||
$('#cb-select-all-1, #cb-select-all-2').prop('checked', this.checked);
|
$('#cb-select-all-1, #cb-select-all-2').prop('checked', this.checked);
|
||||||
@@ -1445,6 +1557,102 @@ final class AdminController
|
|||||||
$('#bulk-action-selector').val(bottomAction);
|
$('#bulk-action-selector').val(bottomAction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Transfer modal
|
||||||
|
var $transferModal = $('#wclp-transfer-modal');
|
||||||
|
$('.wclp-transfer-link').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var licenseId = $(this).data('license-id');
|
||||||
|
var currentDomain = $(this).data('current-domain');
|
||||||
|
$('#transfer-license-id').val(licenseId);
|
||||||
|
$('#transfer-current-domain').text(currentDomain);
|
||||||
|
$('#transfer-new-domain').val('');
|
||||||
|
$transferModal.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test License modal
|
||||||
|
var $testModal = $('#wclp-test-modal');
|
||||||
|
var $testLoading = $('#wclp-test-loading');
|
||||||
|
var $testResult = $('#wclp-test-result');
|
||||||
|
var $testResultContent = $('#wclp-test-result-content');
|
||||||
|
|
||||||
|
$('.wclp-test-license-link').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var licenseKey = $(this).data('license-key');
|
||||||
|
var domain = $(this).data('domain');
|
||||||
|
|
||||||
|
$('#test-license-key').text(licenseKey);
|
||||||
|
$('#test-domain').text(domain);
|
||||||
|
$testLoading.show();
|
||||||
|
$testResult.hide();
|
||||||
|
$testModal.show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: wclpAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wclp_test_license',
|
||||||
|
nonce: wclpAdmin.editNonce,
|
||||||
|
license_key: licenseKey,
|
||||||
|
domain: domain
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
$testLoading.hide();
|
||||||
|
if (response.success) {
|
||||||
|
var result = response.data;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
html = '<div class="notice notice-success inline"><p><strong>✓ <?php echo esc_js(__('License is VALID', 'wc-licensed-product')); ?></strong></p></div>';
|
||||||
|
html += '<table class="widefat striped"><tbody>';
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Product', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Version', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
|
||||||
|
if (result.expires_at) {
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
||||||
|
} else {
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td><?php echo esc_js(__('Lifetime', 'wc-licensed-product')); ?></td></tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html = '<div class="notice notice-error inline"><p><strong>✗ <?php echo esc_js(__('License is INVALID', 'wc-licensed-product')); ?></strong></p></div>';
|
||||||
|
html += '<table class="widefat striped"><tbody>';
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Error Code', 'wc-licensed-product')); ?></th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
|
||||||
|
html += '<tr><th><?php echo esc_js(__('Message', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
|
||||||
|
html += '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$testResultContent.html(html);
|
||||||
|
$testResult.show();
|
||||||
|
} else {
|
||||||
|
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
|
||||||
|
$testResult.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$testLoading.hide();
|
||||||
|
$testResultContent.html('<div class="notice notice-error inline"><p><?php echo esc_js(__('Failed to test license. Please try again.', 'wc-licensed-product')); ?></p></div>');
|
||||||
|
$testResult.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modals
|
||||||
|
$('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
|
||||||
|
$(this).closest('.wclp-modal').hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on('click', function(e) {
|
||||||
|
if ($(e.target).hasClass('wclp-modal')) {
|
||||||
|
$(e.target).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
147
src/Admin/DashboardWidgetController.php
Normal file
147
src/Admin/DashboardWidgetController.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard Widget Controller
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Admin;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\License;
|
||||||
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the WordPress admin dashboard widget for license statistics
|
||||||
|
*/
|
||||||
|
final class DashboardWidgetController
|
||||||
|
{
|
||||||
|
private LicenseManager $licenseManager;
|
||||||
|
|
||||||
|
public function __construct(LicenseManager $licenseManager)
|
||||||
|
{
|
||||||
|
$this->licenseManager = $licenseManager;
|
||||||
|
$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_license_statistics',
|
||||||
|
__('License Statistics', 'wc-licensed-product'),
|
||||||
|
[$this, 'renderWidget']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the dashboard widget content
|
||||||
|
*/
|
||||||
|
public function renderWidget(): void
|
||||||
|
{
|
||||||
|
$stats = $this->licenseManager->getStatistics();
|
||||||
|
$licensesUrl = admin_url('admin.php?page=wc-licenses');
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
.wclp-widget-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.wclp-stat-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e2e4e7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.wclp-stat-card.highlight {
|
||||||
|
border-left: 3px solid #7f54b3;
|
||||||
|
}
|
||||||
|
.wclp-stat-card.warning {
|
||||||
|
border-left: 3px solid #f0b849;
|
||||||
|
}
|
||||||
|
.wclp-stat-card.danger {
|
||||||
|
border-left: 3px solid #dc3232;
|
||||||
|
}
|
||||||
|
.wclp-stat-card.success {
|
||||||
|
border-left: 3px solid #46b450;
|
||||||
|
}
|
||||||
|
.wclp-stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.wclp-stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.wclp-widget-footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e2e4e7;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.wclp-widget-footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wclp-widget-stats">
|
||||||
|
<div class="wclp-stat-card highlight">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['total'])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Total Licenses', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card success">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['by_status'][License::STATUS_ACTIVE])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Active', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card warning">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['expiring_soon'])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Expiring Soon', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card danger">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['by_status'][License::STATUS_EXPIRED])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Expired', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-widget-stats">
|
||||||
|
<div class="wclp-stat-card">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['expiring'])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Time-limited', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card">
|
||||||
|
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['lifetime'])); ?></div>
|
||||||
|
<div class="wclp-stat-label"><?php esc_html_e('Lifetime', 'wc-licensed-product'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-widget-footer">
|
||||||
|
<a href="<?php echo esc_url($licensesUrl); ?>" class="button button-secondary">
|
||||||
|
<?php esc_html_e('View All Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Admin;
|
namespace Jeremias\WcLicensedProduct\Admin;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles WooCommerce settings tab for license defaults
|
* Handles WooCommerce settings tab for license defaults
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +21,11 @@ final class SettingsController
|
|||||||
*/
|
*/
|
||||||
public const OPTION_NAME = 'wc_licensed_product_settings';
|
public const OPTION_NAME = 'wc_licensed_product_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab ID
|
||||||
|
*/
|
||||||
|
private const TAB_ID = 'licensed_product';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
@@ -33,8 +40,10 @@ final class SettingsController
|
|||||||
private function registerHooks(): void
|
private function registerHooks(): void
|
||||||
{
|
{
|
||||||
add_filter('woocommerce_settings_tabs_array', [$this, 'addSettingsTab'], 50);
|
add_filter('woocommerce_settings_tabs_array', [$this, 'addSettingsTab'], 50);
|
||||||
add_action('woocommerce_settings_tabs_licensed_product', [$this, 'renderSettingsTab']);
|
add_action('woocommerce_sections_' . self::TAB_ID, [$this, 'outputSections']);
|
||||||
add_action('woocommerce_update_options_licensed_product', [$this, 'saveSettings']);
|
add_action('woocommerce_settings_' . self::TAB_ID, [$this, 'renderSettingsTab']);
|
||||||
|
add_action('woocommerce_update_options_' . self::TAB_ID, [$this, 'saveSettings']);
|
||||||
|
add_action('wp_ajax_wclp_verify_plugin_license', [$this, 'handleVerifyLicense']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,14 +51,119 @@ final class SettingsController
|
|||||||
*/
|
*/
|
||||||
public function addSettingsTab(array $tabs): array
|
public function addSettingsTab(array $tabs): array
|
||||||
{
|
{
|
||||||
$tabs['licensed_product'] = __('Licensed Products', 'wc-licensed-product');
|
$tabs[self::TAB_ID] = __('Licensed Products', 'wc-licensed-product');
|
||||||
return $tabs;
|
return $tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get settings fields
|
* Get available sections
|
||||||
|
*/
|
||||||
|
public function getSections(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'' => __('Plugin License', 'wc-licensed-product'),
|
||||||
|
'defaults' => __('Default Settings', 'wc-licensed-product'),
|
||||||
|
'notifications' => __('Notifications', 'wc-licensed-product'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current section from URL
|
||||||
|
*/
|
||||||
|
private function getCurrentSection(): string
|
||||||
|
{
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
return isset($_GET['section']) ? sanitize_title(wp_unslash($_GET['section'])) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output sections navigation (sub-tabs)
|
||||||
|
*/
|
||||||
|
public function outputSections(): void
|
||||||
|
{
|
||||||
|
$sections = $this->getSections();
|
||||||
|
|
||||||
|
if (empty($sections) || count($sections) <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentSection = $this->getCurrentSection();
|
||||||
|
|
||||||
|
echo '<ul class="subsubsub">';
|
||||||
|
|
||||||
|
$arrayKeys = array_keys($sections);
|
||||||
|
|
||||||
|
foreach ($sections as $id => $label) {
|
||||||
|
$url = admin_url('admin.php?page=wc-settings&tab=' . self::TAB_ID . '§ion=' . sanitize_title($id));
|
||||||
|
$class = ($currentSection === $id) ? 'current' : '';
|
||||||
|
$separator = (end($arrayKeys) === $id) ? '' : ' | ';
|
||||||
|
|
||||||
|
echo '<li><a href="' . esc_url($url) . '" class="' . esc_attr($class) . '">' . esc_html($label) . '</a>' . $separator . '</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</ul><br class="clear" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings fields for the current section
|
||||||
*/
|
*/
|
||||||
public function getSettingsFields(): array
|
public function getSettingsFields(): array
|
||||||
|
{
|
||||||
|
$currentSection = $this->getCurrentSection();
|
||||||
|
|
||||||
|
return match ($currentSection) {
|
||||||
|
'defaults' => $this->getDefaultsSettings(),
|
||||||
|
'notifications' => $this->getNotificationsSettings(),
|
||||||
|
default => $this->getPluginLicenseSettings(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin license settings (default section)
|
||||||
|
*/
|
||||||
|
private function getPluginLicenseSettings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'plugin_license_section_title' => [
|
||||||
|
'name' => __('Plugin License', 'wc-licensed-product'),
|
||||||
|
'type' => 'title',
|
||||||
|
'desc' => __('Configure the license for this plugin. A valid license is required for frontend features to work.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_section_plugin_license',
|
||||||
|
],
|
||||||
|
'plugin_license_server_url' => [
|
||||||
|
'name' => __('License Server URL', 'wc-licensed-product'),
|
||||||
|
'type' => 'url',
|
||||||
|
'desc' => __('The URL of the license server (e.g., https://shop.example.com).', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_plugin_license_server_url',
|
||||||
|
'default' => '',
|
||||||
|
'placeholder' => 'https://shop.example.com',
|
||||||
|
],
|
||||||
|
'plugin_license_key' => [
|
||||||
|
'name' => __('License Key', 'wc-licensed-product'),
|
||||||
|
'type' => 'text',
|
||||||
|
'desc' => __('Your license key in XXXX-XXXX-XXXX-XXXX format.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_plugin_license_key',
|
||||||
|
'default' => '',
|
||||||
|
'placeholder' => 'XXXX-XXXX-XXXX-XXXX',
|
||||||
|
],
|
||||||
|
'plugin_license_server_secret' => [
|
||||||
|
'name' => __('Server Secret (Optional)', 'wc-licensed-product'),
|
||||||
|
'type' => 'password',
|
||||||
|
'desc' => __('If the license server uses signed responses, enter the shared secret here for enhanced security.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_plugin_license_server_secret',
|
||||||
|
'default' => '',
|
||||||
|
],
|
||||||
|
'plugin_license_section_end' => [
|
||||||
|
'type' => 'sectionend',
|
||||||
|
'id' => 'wc_licensed_product_section_plugin_license_end',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default license settings
|
||||||
|
*/
|
||||||
|
private function getDefaultsSettings(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'section_title' => [
|
'section_title' => [
|
||||||
@@ -92,7 +206,15 @@ final class SettingsController
|
|||||||
'type' => 'sectionend',
|
'type' => 'sectionend',
|
||||||
'id' => 'wc_licensed_product_section_defaults_end',
|
'id' => 'wc_licensed_product_section_defaults_end',
|
||||||
],
|
],
|
||||||
// Email settings section
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notifications settings
|
||||||
|
*/
|
||||||
|
private function getNotificationsSettings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
'email_section_title' => [
|
'email_section_title' => [
|
||||||
'name' => __('Expiration Warning Schedule', 'wc-licensed-product'),
|
'name' => __('Expiration Warning Schedule', 'wc-licensed-product'),
|
||||||
'type' => 'title',
|
'type' => 'title',
|
||||||
@@ -138,9 +260,96 @@ final class SettingsController
|
|||||||
*/
|
*/
|
||||||
public function renderSettingsTab(): void
|
public function renderSettingsTab(): void
|
||||||
{
|
{
|
||||||
|
$currentSection = $this->getCurrentSection();
|
||||||
|
|
||||||
|
// Only show license status on the plugin license section
|
||||||
|
if ($currentSection === '') {
|
||||||
|
$this->renderLicenseStatus();
|
||||||
|
}
|
||||||
|
|
||||||
woocommerce_admin_fields($this->getSettingsFields());
|
woocommerce_admin_fields($this->getSettingsFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render license status notice
|
||||||
|
*/
|
||||||
|
private function renderLicenseStatus(): void
|
||||||
|
{
|
||||||
|
$checker = PluginLicenseChecker::getInstance();
|
||||||
|
|
||||||
|
if ($checker->isLocalhost()) {
|
||||||
|
echo '<div class="notice notice-info inline"><p>';
|
||||||
|
echo '<span class="dashicons dashicons-info" style="color: #00a0d2;"></span> ';
|
||||||
|
echo esc_html__('Running on localhost - license validation bypassed.', 'wc-licensed-product');
|
||||||
|
echo '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($checker->isLicenseValid()) {
|
||||||
|
echo '<div class="notice notice-success inline"><p>';
|
||||||
|
echo '<span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span> ';
|
||||||
|
echo esc_html__('License is valid and active.', 'wc-licensed-product');
|
||||||
|
echo '</p></div>';
|
||||||
|
} else {
|
||||||
|
$error = $checker->getLastError();
|
||||||
|
echo '<div class="notice notice-error inline"><p>';
|
||||||
|
echo '<span class="dashicons dashicons-warning" style="color: #dc3232;"></span> ';
|
||||||
|
echo esc_html__('License is not valid. Frontend features are disabled.', 'wc-licensed-product');
|
||||||
|
if ($error) {
|
||||||
|
echo '<br><small>' . esc_html($error) . '</small>';
|
||||||
|
}
|
||||||
|
echo '</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add verify button
|
||||||
|
$nonce = wp_create_nonce('wclp_verify_license');
|
||||||
|
echo '<p>';
|
||||||
|
echo '<button type="button" class="button" id="wclp-verify-license" data-nonce="' . esc_attr($nonce) . '">';
|
||||||
|
echo esc_html__('Verify License', 'wc-licensed-product');
|
||||||
|
echo '</button>';
|
||||||
|
echo '<span id="wclp-verify-result" style="margin-left: 10px;"></span>';
|
||||||
|
echo '</p>';
|
||||||
|
|
||||||
|
// Inline script for verify button
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(function($) {
|
||||||
|
$('#wclp-verify-license').on('click', function() {
|
||||||
|
var $btn = $(this);
|
||||||
|
var $result = $('#wclp-verify-result');
|
||||||
|
var nonce = $btn.data('nonce');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('<?php echo esc_js(__('Verifying...', 'wc-licensed-product')); ?>');
|
||||||
|
$result.text('');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wclp_verify_plugin_license',
|
||||||
|
nonce: nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$result.html('<span style="color: #46b450;">' + response.data.message + '</span>');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
$result.html('<span style="color: #dc3232;">' + response.data.message + '</span>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$result.html('<span style="color: #dc3232;"><?php echo esc_js(__('Request failed.', 'wc-licensed-product')); ?></span>');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Verify License', 'wc-licensed-product')); ?>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save settings
|
* Save settings
|
||||||
*/
|
*/
|
||||||
@@ -210,4 +419,55 @@ final class SettingsController
|
|||||||
$value = get_option('wc_licensed_product_expiration_warning_days_second', 1);
|
$value = get_option('wc_licensed_product_expiration_warning_days_second', 1);
|
||||||
return max(0, (int) $value);
|
return max(0, (int) $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin license server URL
|
||||||
|
*/
|
||||||
|
public static function getPluginLicenseServerUrl(): string
|
||||||
|
{
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_server_url', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin license key
|
||||||
|
*/
|
||||||
|
public static function getPluginLicenseKey(): string
|
||||||
|
{
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin license server secret
|
||||||
|
*/
|
||||||
|
public static function getPluginLicenseServerSecret(): ?string
|
||||||
|
{
|
||||||
|
$secret = get_option('wc_licensed_product_plugin_license_server_secret', '');
|
||||||
|
return !empty($secret) ? (string) $secret : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle AJAX verify license request
|
||||||
|
*/
|
||||||
|
public function handleVerifyLicense(): void
|
||||||
|
{
|
||||||
|
if (!check_ajax_referer('wclp_verify_license', 'nonce', false)) {
|
||||||
|
wp_send_json_error(['message' => __('Security check failed.', 'wc-licensed-product')], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error(['message' => __('Insufficient permissions.', 'wc-licensed-product')], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$checker = PluginLicenseChecker::getInstance();
|
||||||
|
$checker->clearCache();
|
||||||
|
|
||||||
|
$valid = $checker->validateLicense(true);
|
||||||
|
|
||||||
|
if ($valid) {
|
||||||
|
wp_send_json_success(['message' => __('License verified successfully!', 'wc-licensed-product')]);
|
||||||
|
} else {
|
||||||
|
$error = $checker->getLastError() ?: __('License validation failed.', 'wc-licensed-product');
|
||||||
|
wp_send_json_error(['message' => $error]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,14 @@ final class VersionAdminController
|
|||||||
<tr id="sha256-hash-row" style="display: none;">
|
<tr id="sha256-hash-row" style="display: none;">
|
||||||
<th><label for="new_checksum_file"><?php esc_html_e('Checksum File', 'wc-licensed-product'); ?></label></th>
|
<th><label for="new_checksum_file"><?php esc_html_e('Checksum File', 'wc-licensed-product'); ?></label></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="file" id="new_checksum_file" name="new_checksum_file" accept=".sha256,.txt" />
|
<input type="file" id="new_checksum_file" name="new_checksum_file" accept=".sha256,.txt" style="display: none;" />
|
||||||
|
<span id="selected_checksum_name" class="selected-file-name"></span>
|
||||||
|
<button type="button" class="button" id="select-checksum-file-btn">
|
||||||
|
<?php esc_html_e('Select Checksum File', 'wc-licensed-product'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="remove-checksum-file-btn" style="display: none;">
|
||||||
|
<?php esc_html_e('Remove', 'wc-licensed-product'); ?>
|
||||||
|
</button>
|
||||||
<p class="description"><?php esc_html_e('Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity.', 'wc-licensed-product'); ?></p>
|
<p class="description"><?php esc_html_e('Upload a SHA256 checksum file (.sha256 or .txt) to verify file integrity.', 'wc-licensed-product'); ?></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -128,6 +135,7 @@ final class VersionAdminController
|
|||||||
<tr>
|
<tr>
|
||||||
<th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Version', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Download File', 'wc-licensed-product'); ?></th>
|
||||||
|
<th><?php esc_html_e('SHA256', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Release Notes', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
||||||
<th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th>
|
<th><?php esc_html_e('Released', 'wc-licensed-product'); ?></th>
|
||||||
@@ -137,7 +145,7 @@ final class VersionAdminController
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($versions)): ?>
|
<?php if (empty($versions)): ?>
|
||||||
<tr class="no-versions">
|
<tr class="no-versions">
|
||||||
<td colspan="6"><?php esc_html_e('No versions found. Add your first version above.', 'wc-licensed-product'); ?></td>
|
<td colspan="7"><?php esc_html_e('No versions found. Add your first version above.', 'wc-licensed-product'); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($versions as $version): ?>
|
<?php foreach ($versions as $version): ?>
|
||||||
@@ -149,16 +157,25 @@ final class VersionAdminController
|
|||||||
$filename = $version->getDownloadFilename();
|
$filename = $version->getDownloadFilename();
|
||||||
if ($effectiveUrl):
|
if ($effectiveUrl):
|
||||||
?>
|
?>
|
||||||
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
<span class="version-download-link">
|
||||||
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
||||||
</a>
|
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
||||||
<?php if ($version->getAttachmentId()): ?>
|
</a>
|
||||||
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
<?php if ($version->getAttachmentId()): ?>
|
||||||
<?php endif; ?>
|
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($version->getFileHash()): ?>
|
||||||
|
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
|
||||||
|
<?php else: ?>
|
||||||
|
<em>—</em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
||||||
@@ -344,7 +361,7 @@ final class VersionAdminController
|
|||||||
wp_send_json_error(['message' => __('Version ID is required.', 'wc-licensed-product')]);
|
wp_send_json_error(['message' => __('Version ID is required.', 'wc-licensed-product')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->versionManager->updateVersion($versionId, null, null, !$currentlyActive);
|
$result = $this->versionManager->updateVersion($versionId, null, !$currentlyActive, null);
|
||||||
|
|
||||||
if (!$result) {
|
if (!$result) {
|
||||||
wp_send_json_error(['message' => __('Failed to update version.', 'wc-licensed-product')]);
|
wp_send_json_error(['message' => __('Failed to update version.', 'wc-licensed-product')]);
|
||||||
@@ -371,16 +388,25 @@ final class VersionAdminController
|
|||||||
$filename = $version->getDownloadFilename();
|
$filename = $version->getDownloadFilename();
|
||||||
if ($effectiveUrl):
|
if ($effectiveUrl):
|
||||||
?>
|
?>
|
||||||
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
<span class="version-download-link">
|
||||||
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
<a href="<?php echo esc_url($effectiveUrl); ?>" target="_blank">
|
||||||
</a>
|
<?php echo esc_html($filename ?: wp_basename($effectiveUrl)); ?>
|
||||||
<?php if ($version->getAttachmentId()): ?>
|
</a>
|
||||||
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
<?php if ($version->getAttachmentId()): ?>
|
||||||
<?php endif; ?>
|
<span class="dashicons dashicons-media-archive" title="<?php esc_attr_e('Uploaded file', 'wc-licensed-product'); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
<em><?php esc_html_e('No download file', 'wc-licensed-product'); ?></em>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($version->getFileHash()): ?>
|
||||||
|
<code class="file-hash" title="<?php echo esc_attr($version->getFileHash()); ?>"><?php echo esc_html(substr($version->getFileHash(), 0, 12)); ?>...</code>
|
||||||
|
<?php else: ?>
|
||||||
|
<em>—</em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
<td><?php echo esc_html($version->getReleaseNotes() ? wp_trim_words($version->getReleaseNotes(), 10) : '—'); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
<span class="version-status version-status-<?php echo $version->isActive() ? 'active' : 'inactive'; ?>">
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ final class ResponseSigner
|
|||||||
$timestamp = time();
|
$timestamp = time();
|
||||||
$signingKey = $this->deriveKey($licenseKey);
|
$signingKey = $this->deriveKey($licenseKey);
|
||||||
|
|
||||||
// Sort keys for consistent ordering
|
// Recursively sort keys for consistent ordering (required by client implementation)
|
||||||
ksort($data);
|
$data = $this->recursiveKeySort($data);
|
||||||
|
|
||||||
// Build signature payload
|
// Build signature payload
|
||||||
$payload = $timestamp . ':' . json_encode(
|
$payload = $timestamp . ':' . json_encode(
|
||||||
@@ -109,6 +109,33 @@ final class ResponseSigner
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively sort array keys alphabetically
|
||||||
|
*
|
||||||
|
* @param mixed $data The data to sort
|
||||||
|
* @return mixed The sorted data
|
||||||
|
*/
|
||||||
|
private function recursiveKeySort(mixed $data): mixed
|
||||||
|
{
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if array is associative (has string keys)
|
||||||
|
$isAssociative = array_keys($data) !== range(0, count($data) - 1);
|
||||||
|
|
||||||
|
if ($isAssociative) {
|
||||||
|
ksort($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively sort nested arrays
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$data[$key] = $this->recursiveKeySort($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a unique signing key for a license
|
* Derive a unique signing key for a license
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -95,29 +95,152 @@ final class RestApiController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get client IP address
|
* Get client IP address
|
||||||
|
*
|
||||||
|
* Security note: Only trust proxy headers when explicitly configured.
|
||||||
|
* Set WC_LICENSE_TRUSTED_PROXIES constant or configure trusted_proxies
|
||||||
|
* in wp-config.php to enable proxy header support.
|
||||||
|
*
|
||||||
|
* @return string Client IP address
|
||||||
*/
|
*/
|
||||||
private function getClientIp(): string
|
private function getClientIp(): string
|
||||||
{
|
{
|
||||||
$headers = [
|
// Get the direct connection IP first
|
||||||
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
'HTTP_X_FORWARDED_FOR',
|
|
||||||
'HTTP_X_REAL_IP',
|
|
||||||
'REMOTE_ADDR',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($headers as $header) {
|
// Only check proxy headers if we're behind a trusted proxy
|
||||||
if (!empty($_SERVER[$header])) {
|
if ($this->isTrustedProxy($remoteAddr)) {
|
||||||
$ips = explode(',', $_SERVER[$header]);
|
// Check headers in order of trust preference
|
||||||
$ip = trim($ips[0]);
|
$headers = [
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||||
return $ip;
|
'HTTP_X_FORWARDED_FOR',
|
||||||
|
'HTTP_X_REAL_IP',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (!empty($_SERVER[$header])) {
|
||||||
|
$ips = explode(',', $_SERVER[$header]);
|
||||||
|
$ip = trim($ips[0]);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate and return direct connection IP
|
||||||
|
if (filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
|
||||||
|
return $remoteAddr;
|
||||||
|
}
|
||||||
|
|
||||||
return '0.0.0.0';
|
return '0.0.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given IP is a trusted proxy
|
||||||
|
*
|
||||||
|
* @param string $ip The IP address to check
|
||||||
|
* @return bool Whether the IP is a trusted proxy
|
||||||
|
*/
|
||||||
|
private function isTrustedProxy(string $ip): bool
|
||||||
|
{
|
||||||
|
// Check if trusted proxies are configured
|
||||||
|
if (!defined('WC_LICENSE_TRUSTED_PROXIES')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trustedProxies = WC_LICENSE_TRUSTED_PROXIES;
|
||||||
|
|
||||||
|
// Handle string constant (comma-separated list)
|
||||||
|
if (is_string($trustedProxies)) {
|
||||||
|
$trustedProxies = array_map('trim', explode(',', $trustedProxies));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($trustedProxies)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for special keywords
|
||||||
|
if (in_array('CLOUDFLARE', $trustedProxies, true)) {
|
||||||
|
// Cloudflare IP ranges (simplified - in production, fetch from Cloudflare API)
|
||||||
|
if ($this->isCloudflareIp($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check direct IP match or CIDR notation
|
||||||
|
foreach ($trustedProxies as $proxy) {
|
||||||
|
if ($proxy === $ip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support CIDR notation
|
||||||
|
if (str_contains($proxy, '/') && $this->ipMatchesCidr($ip, $proxy)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if IP is in Cloudflare range
|
||||||
|
*
|
||||||
|
* @param string $ip The IP to check
|
||||||
|
* @return bool Whether IP belongs to Cloudflare
|
||||||
|
*/
|
||||||
|
private function isCloudflareIp(string $ip): bool
|
||||||
|
{
|
||||||
|
// Cloudflare IPv4 ranges (as of 2024)
|
||||||
|
$cloudflareRanges = [
|
||||||
|
'173.245.48.0/20',
|
||||||
|
'103.21.244.0/22',
|
||||||
|
'103.22.200.0/22',
|
||||||
|
'103.31.4.0/22',
|
||||||
|
'141.101.64.0/18',
|
||||||
|
'108.162.192.0/18',
|
||||||
|
'190.93.240.0/20',
|
||||||
|
'188.114.96.0/20',
|
||||||
|
'197.234.240.0/22',
|
||||||
|
'198.41.128.0/17',
|
||||||
|
'162.158.0.0/15',
|
||||||
|
'104.16.0.0/13',
|
||||||
|
'104.24.0.0/14',
|
||||||
|
'172.64.0.0/13',
|
||||||
|
'131.0.72.0/22',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cloudflareRanges as $range) {
|
||||||
|
if ($this->ipMatchesCidr($ip, $range)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP matches a CIDR range
|
||||||
|
*
|
||||||
|
* @param string $ip The IP to check
|
||||||
|
* @param string $cidr The CIDR range (e.g., "192.168.1.0/24")
|
||||||
|
* @return bool Whether the IP matches the CIDR range
|
||||||
|
*/
|
||||||
|
private function ipMatchesCidr(string $ip, string $cidr): bool
|
||||||
|
{
|
||||||
|
[$subnet, $bits] = explode('/', $cidr);
|
||||||
|
|
||||||
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
|
||||||
|
!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipLong = ip2long($ip);
|
||||||
|
$subnetLong = ip2long($subnet);
|
||||||
|
$mask = -1 << (32 - (int) $bits);
|
||||||
|
|
||||||
|
return ($ipLong & $mask) === ($subnetLong & $mask);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register REST API routes
|
* Register REST API routes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ final class LicenseEmailController
|
|||||||
public function registerEmailClasses(array $email_classes): array
|
public function registerEmailClasses(array $email_classes): array
|
||||||
{
|
{
|
||||||
$email_classes['WCLP_License_Expiration'] = new LicenseExpirationEmail();
|
$email_classes['WCLP_License_Expiration'] = new LicenseExpirationEmail();
|
||||||
|
$email_classes['WCLP_License_Expired'] = new LicenseExpiredEmail();
|
||||||
return $email_classes;
|
return $email_classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +70,13 @@ final class LicenseEmailController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send expiration warning emails
|
* Send expiration warning emails and auto-expire licenses
|
||||||
*/
|
*/
|
||||||
public function sendExpirationWarnings(): void
|
public function sendExpirationWarnings(): void
|
||||||
{
|
{
|
||||||
|
// First, auto-expire licenses that have passed their expiration date
|
||||||
|
$this->autoExpireAndNotify();
|
||||||
|
|
||||||
// Check if expiration emails are enabled in settings
|
// Check if expiration emails are enabled in settings
|
||||||
if (!SettingsController::isExpirationEmailsEnabled()) {
|
if (!SettingsController::isExpirationEmailsEnabled()) {
|
||||||
return;
|
return;
|
||||||
@@ -107,6 +111,41 @@ final class LicenseEmailController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-expire licenses and send expired notifications
|
||||||
|
*/
|
||||||
|
private function autoExpireAndNotify(): void
|
||||||
|
{
|
||||||
|
// Get licenses that should be auto-expired
|
||||||
|
$expiredActiveLicenses = $this->licenseManager->getExpiredActiveLicenses();
|
||||||
|
|
||||||
|
if (empty($expiredActiveLicenses)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the WooCommerce email instance for expired notifications
|
||||||
|
$mailer = WC()->mailer();
|
||||||
|
$emails = $mailer->get_emails();
|
||||||
|
|
||||||
|
/** @var LicenseExpiredEmail|null $expiredEmail */
|
||||||
|
$expiredEmail = $emails['WCLP_License_Expired'] ?? null;
|
||||||
|
|
||||||
|
foreach ($expiredActiveLicenses as $license) {
|
||||||
|
// Auto-expire the license
|
||||||
|
$wasExpired = $this->licenseManager->autoExpireLicense($license->getId());
|
||||||
|
|
||||||
|
if ($wasExpired && $expiredEmail && $expiredEmail->is_enabled()) {
|
||||||
|
// Check if we haven't already sent an expired notification
|
||||||
|
if (!$this->licenseManager->wasExpirationNotified($license->getId(), 'license_expired')) {
|
||||||
|
// Send expired notification email
|
||||||
|
if ($expiredEmail->trigger($license)) {
|
||||||
|
$this->licenseManager->markExpirationNotified($license->getId(), 'license_expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process and send expiration warnings for a specific time frame
|
* Process and send expiration warnings for a specific time frame
|
||||||
*
|
*
|
||||||
|
|||||||
335
src/Email/LicenseExpiredEmail.php
Normal file
335
src/Email/LicenseExpiredEmail.php
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* License Expired Email
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Email
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Email;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\License;
|
||||||
|
use WC_Email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* License Expired Email class
|
||||||
|
*
|
||||||
|
* Sends email notifications to customers when their licenses have expired.
|
||||||
|
* Uses WooCommerce's transactional email system for consistent styling and customization.
|
||||||
|
*/
|
||||||
|
class LicenseExpiredEmail extends WC_Email
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* License object
|
||||||
|
*/
|
||||||
|
public ?License $license = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product name
|
||||||
|
*/
|
||||||
|
public string $product_name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expiration date formatted
|
||||||
|
*/
|
||||||
|
public string $expiration_date = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customer display name
|
||||||
|
*/
|
||||||
|
public string $customer_name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->id = 'wclp_license_expired';
|
||||||
|
$this->customer_email = true;
|
||||||
|
$this->title = __('License Expired', 'wc-licensed-product');
|
||||||
|
$this->description = __('License expired emails are sent to customers when their licenses have expired.', 'wc-licensed-product');
|
||||||
|
|
||||||
|
$this->placeholders = [
|
||||||
|
'{site_title}' => $this->get_blogname(),
|
||||||
|
'{product_name}' => '',
|
||||||
|
'{expiration_date}' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Call parent constructor
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email subject
|
||||||
|
*/
|
||||||
|
public function get_default_subject(): string
|
||||||
|
{
|
||||||
|
return __('[{site_title}] Your license for {product_name} has expired', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email heading
|
||||||
|
*/
|
||||||
|
public function get_default_heading(): string
|
||||||
|
{
|
||||||
|
return __('License Expired', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger the email
|
||||||
|
*
|
||||||
|
* @param License $license License object
|
||||||
|
*/
|
||||||
|
public function trigger(License $license): bool
|
||||||
|
{
|
||||||
|
$this->setup_locale();
|
||||||
|
|
||||||
|
$customer = get_userdata($license->getCustomerId());
|
||||||
|
if (!$customer || !$customer->user_email) {
|
||||||
|
$this->restore_locale();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->license = $license;
|
||||||
|
$this->recipient = $customer->user_email;
|
||||||
|
$this->customer_name = $customer->display_name ?: __('Customer', 'wc-licensed-product');
|
||||||
|
|
||||||
|
$product = wc_get_product($license->getProductId());
|
||||||
|
$this->product_name = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||||
|
|
||||||
|
$expiresAt = $license->getExpiresAt();
|
||||||
|
$this->expiration_date = $expiresAt ? $expiresAt->format(get_option('date_format')) : '';
|
||||||
|
|
||||||
|
// Update placeholders
|
||||||
|
$this->placeholders['{product_name}'] = $this->product_name;
|
||||||
|
$this->placeholders['{expiration_date}'] = $this->expiration_date;
|
||||||
|
|
||||||
|
if (!$this->is_enabled() || !$this->get_recipient()) {
|
||||||
|
$this->restore_locale();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->send(
|
||||||
|
$this->get_recipient(),
|
||||||
|
$this->get_subject(),
|
||||||
|
$this->get_content(),
|
||||||
|
$this->get_headers(),
|
||||||
|
$this->get_attachments()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->restore_locale();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content HTML
|
||||||
|
*/
|
||||||
|
public function get_content_html(): string
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Use WooCommerce's email header
|
||||||
|
wc_get_template('emails/email-header.php', ['email_heading' => $this->get_heading()]);
|
||||||
|
|
||||||
|
$this->render_email_body_html();
|
||||||
|
|
||||||
|
// Use WooCommerce's email footer
|
||||||
|
wc_get_template('emails/email-footer.php', ['email' => $this]);
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content plain text
|
||||||
|
*/
|
||||||
|
public function get_content_plain(): string
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
|
||||||
|
echo esc_html(wp_strip_all_tags($this->get_heading()));
|
||||||
|
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||||
|
|
||||||
|
$this->render_email_body_plain();
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML email body content
|
||||||
|
*/
|
||||||
|
private function render_email_body_html(): void
|
||||||
|
{
|
||||||
|
$account_url = wc_get_account_endpoint_url('licenses');
|
||||||
|
?>
|
||||||
|
<p><?php printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($this->customer_name)); ?></p>
|
||||||
|
|
||||||
|
<p style="color: #dc3232; font-weight: 600;">
|
||||||
|
<?php printf(
|
||||||
|
esc_html__('Your license for %1$s has expired on %2$s.', 'wc-licensed-product'),
|
||||||
|
'<strong>' . esc_html($this->product_name) . '</strong>',
|
||||||
|
esc_html($this->expiration_date)
|
||||||
|
); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e('Your license is no longer valid and the product will stop working until you renew.', 'wc-licensed-product'); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2><?php esc_html_e('Expired License Details', 'wc-licensed-product'); ?></h2>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 40px;">
|
||||||
|
<table class="td" cellspacing="0" cellpadding="6" style="width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;" border="1">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Product:', 'wc-licensed-product'); ?></th>
|
||||||
|
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php echo esc_html($this->product_name); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></th>
|
||||||
|
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;">
|
||||||
|
<code style="background: #f5f5f5; padding: 3px 8px; border-radius: 3px; font-family: monospace;">
|
||||||
|
<?php echo esc_html($this->license->getLicenseKey()); ?>
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></th>
|
||||||
|
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php echo esc_html($this->license->getDomain()); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Expired on:', 'wc-licensed-product'); ?></th>
|
||||||
|
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>; color: #dc3232; font-weight: 600;"><?php echo esc_html($this->expiration_date); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Status:', 'wc-licensed-product'); ?></th>
|
||||||
|
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;">
|
||||||
|
<span style="background: #f8d7da; color: #721c24; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
||||||
|
<?php esc_html_e('Expired', 'wc-licensed-product'); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$additional_content = $this->get_additional_content();
|
||||||
|
if ($additional_content) :
|
||||||
|
?>
|
||||||
|
<p><?php echo wp_kses_post($additional_content); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p style="margin-top: 25px;">
|
||||||
|
<a href="<?php echo esc_url($account_url); ?>" class="button" style="display: inline-block; background-color: #7f54b3; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: 600;">
|
||||||
|
<?php esc_html_e('View My Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render plain text email body content
|
||||||
|
*/
|
||||||
|
private function render_email_body_plain(): void
|
||||||
|
{
|
||||||
|
printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($this->customer_name));
|
||||||
|
echo "\n\n";
|
||||||
|
|
||||||
|
printf(
|
||||||
|
esc_html__('Your license for %1$s has expired on %2$s.', 'wc-licensed-product'),
|
||||||
|
esc_html($this->product_name),
|
||||||
|
esc_html($this->expiration_date)
|
||||||
|
);
|
||||||
|
echo "\n\n";
|
||||||
|
|
||||||
|
echo esc_html__('Your license is no longer valid and the product will stop working until you renew.', 'wc-licensed-product');
|
||||||
|
echo "\n\n";
|
||||||
|
|
||||||
|
echo "----------\n";
|
||||||
|
echo esc_html__('Expired License Details', 'wc-licensed-product') . "\n";
|
||||||
|
echo "----------\n\n";
|
||||||
|
|
||||||
|
echo esc_html__('Product:', 'wc-licensed-product') . ' ' . esc_html($this->product_name) . "\n";
|
||||||
|
echo esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($this->license->getLicenseKey()) . "\n";
|
||||||
|
echo esc_html__('Domain:', 'wc-licensed-product') . ' ' . esc_html($this->license->getDomain()) . "\n";
|
||||||
|
echo esc_html__('Expired on:', 'wc-licensed-product') . ' ' . esc_html($this->expiration_date) . "\n";
|
||||||
|
echo esc_html__('Status:', 'wc-licensed-product') . ' ' . esc_html__('Expired', 'wc-licensed-product') . "\n\n";
|
||||||
|
|
||||||
|
$additional_content = $this->get_additional_content();
|
||||||
|
if ($additional_content) {
|
||||||
|
echo "----------\n\n";
|
||||||
|
echo esc_html(wp_strip_all_tags(wptexturize($additional_content)));
|
||||||
|
echo "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo esc_html__('View My Licenses', 'wc-licensed-product') . ': ' . esc_url(wc_get_account_endpoint_url('licenses')) . "\n\n";
|
||||||
|
|
||||||
|
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default content to show below main email content
|
||||||
|
*/
|
||||||
|
public function get_default_additional_content(): string
|
||||||
|
{
|
||||||
|
return __('To continue using this product, please renew your license.', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize settings form fields
|
||||||
|
*/
|
||||||
|
public function init_form_fields(): void
|
||||||
|
{
|
||||||
|
$placeholder_text = sprintf(
|
||||||
|
/* translators: %s: list of placeholders */
|
||||||
|
__('Available placeholders: %s', 'wc-licensed-product'),
|
||||||
|
'<code>{site_title}, {product_name}, {expiration_date}</code>'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->form_fields = [
|
||||||
|
'enabled' => [
|
||||||
|
'title' => __('Enable/Disable', 'wc-licensed-product'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('Enable this email notification', 'wc-licensed-product'),
|
||||||
|
'default' => 'yes',
|
||||||
|
],
|
||||||
|
'subject' => [
|
||||||
|
'title' => __('Subject', 'wc-licensed-product'),
|
||||||
|
'type' => 'text',
|
||||||
|
'desc_tip' => true,
|
||||||
|
'description' => $placeholder_text,
|
||||||
|
'placeholder' => $this->get_default_subject(),
|
||||||
|
'default' => '',
|
||||||
|
],
|
||||||
|
'heading' => [
|
||||||
|
'title' => __('Email heading', 'wc-licensed-product'),
|
||||||
|
'type' => 'text',
|
||||||
|
'desc_tip' => true,
|
||||||
|
'description' => $placeholder_text,
|
||||||
|
'placeholder' => $this->get_default_heading(),
|
||||||
|
'default' => '',
|
||||||
|
],
|
||||||
|
'additional_content' => [
|
||||||
|
'title' => __('Additional content', 'wc-licensed-product'),
|
||||||
|
'description' => __('Text to appear below the main email content.', 'wc-licensed-product') . ' ' . $placeholder_text,
|
||||||
|
'css' => 'width:400px; height: 75px;',
|
||||||
|
'placeholder' => $this->get_default_additional_content(),
|
||||||
|
'type' => 'textarea',
|
||||||
|
'default' => '',
|
||||||
|
'desc_tip' => true,
|
||||||
|
],
|
||||||
|
'email_type' => [
|
||||||
|
'title' => __('Email type', 'wc-licensed-product'),
|
||||||
|
'type' => 'select',
|
||||||
|
'description' => __('Choose which format of email to send.', 'wc-licensed-product'),
|
||||||
|
'default' => 'html',
|
||||||
|
'class' => 'email_type wc-enhanced-select',
|
||||||
|
'options' => $this->get_email_type_options(),
|
||||||
|
'desc_tip' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,6 +129,7 @@ final class AccountController
|
|||||||
),
|
),
|
||||||
'release_notes' => $version->getReleaseNotes(),
|
'release_notes' => $version->getReleaseNotes(),
|
||||||
'released_at' => $version->getReleasedAt()->format(get_option('date_format')),
|
'released_at' => $version->getReleasedAt()->format(get_option('date_format')),
|
||||||
|
'file_hash' => $version->getFileHash(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -862,6 +862,56 @@ class LicenseManager
|
|||||||
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get licenses that have passed their expiration date but are still marked as active
|
||||||
|
*
|
||||||
|
* @return array Array of License objects that need to be auto-expired
|
||||||
|
*/
|
||||||
|
public function getExpiredActiveLicenses(): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$now = new \DateTimeImmutable();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM {$tableName}
|
||||||
|
WHERE expires_at IS NOT NULL
|
||||||
|
AND expires_at < %s
|
||||||
|
AND status = %s";
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
$wpdb->prepare($sql, $now->format('Y-m-d H:i:s'), License::STATUS_ACTIVE),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-expire a license and return true if status was changed
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @return bool True if license was expired, false if already expired or error
|
||||||
|
*/
|
||||||
|
public function autoExpireLicense(int $licenseId): bool
|
||||||
|
{
|
||||||
|
$license = $this->getLicenseById($licenseId);
|
||||||
|
if (!$license) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only expire if currently active and past expiration date
|
||||||
|
if ($license->getStatus() !== License::STATUS_ACTIVE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$license->isExpired()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->updateLicenseStatus($licenseId, License::STATUS_EXPIRED);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import a license from CSV data
|
* Import a license from CSV data
|
||||||
*
|
*
|
||||||
|
|||||||
287
src/License/PluginLicenseChecker.php
Normal file
287
src/License/PluginLicenseChecker.php
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin License Checker
|
||||||
|
*
|
||||||
|
* Validates the plugin's own license against a remote server.
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\License
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\License;
|
||||||
|
|
||||||
|
use Magdev\WcLicensedProductClient\LicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\LicenseClientInterface;
|
||||||
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||||
|
use Magdev\WcLicensedProductClient\Exception\LicenseException;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles validation of this plugin's license
|
||||||
|
*/
|
||||||
|
final class PluginLicenseChecker
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cache key for license validation result
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wclp_plugin_license_valid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache TTL for successful validation (1 hour)
|
||||||
|
*/
|
||||||
|
private const CACHE_TTL = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache key for error messages
|
||||||
|
*/
|
||||||
|
private const ERROR_CACHE_KEY = 'wclp_plugin_license_error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache TTL for errors (5 minutes)
|
||||||
|
*/
|
||||||
|
private const ERROR_CACHE_TTL = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance
|
||||||
|
*/
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached localhost check result
|
||||||
|
*/
|
||||||
|
private ?bool $isLocalhostCached = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*/
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor for singleton
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
// Private constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the plugin license is valid
|
||||||
|
*
|
||||||
|
* Returns cached result if available, otherwise validates against server.
|
||||||
|
*/
|
||||||
|
public function isLicenseValid(): bool
|
||||||
|
{
|
||||||
|
// Always valid on localhost
|
||||||
|
if ($this->isLocalhost()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
$cached = get_transient(self::CACHE_KEY);
|
||||||
|
if ($cached !== false) {
|
||||||
|
return (bool) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate against server
|
||||||
|
return $this->validateLicense();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate license against the server
|
||||||
|
*
|
||||||
|
* @param bool $forceRefresh Force refresh even if cached
|
||||||
|
* @return bool True if license is valid
|
||||||
|
*/
|
||||||
|
public function validateLicense(bool $forceRefresh = false): bool
|
||||||
|
{
|
||||||
|
// Always valid on localhost
|
||||||
|
if ($this->isLocalhost()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check settings are configured
|
||||||
|
$serverUrl = $this->getLicenseServerUrl();
|
||||||
|
$licenseKey = $this->getLicenseKey();
|
||||||
|
|
||||||
|
if (empty($serverUrl) || empty($licenseKey)) {
|
||||||
|
set_transient(
|
||||||
|
self::ERROR_CACHE_KEY,
|
||||||
|
__('License settings not configured.', 'wc-licensed-product'),
|
||||||
|
self::ERROR_CACHE_TTL
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache unless force refresh
|
||||||
|
if (!$forceRefresh) {
|
||||||
|
$cached = get_transient(self::CACHE_KEY);
|
||||||
|
if ($cached !== false) {
|
||||||
|
return (bool) $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = $this->createLicenseClient();
|
||||||
|
$domain = $this->getCurrentDomain();
|
||||||
|
|
||||||
|
// Validate the license
|
||||||
|
$client->validate($licenseKey, $domain);
|
||||||
|
|
||||||
|
// Valid license - cache success
|
||||||
|
set_transient(self::CACHE_KEY, 1, self::CACHE_TTL);
|
||||||
|
delete_transient(self::ERROR_CACHE_KEY);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (LicenseException $e) {
|
||||||
|
// License-specific error (invalid, expired, revoked, etc.)
|
||||||
|
set_transient(self::CACHE_KEY, 0, self::CACHE_TTL);
|
||||||
|
set_transient(self::ERROR_CACHE_KEY, $e->getMessage(), self::ERROR_CACHE_TTL);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Network/server error - use shorter cache to allow retry
|
||||||
|
set_transient(
|
||||||
|
self::ERROR_CACHE_KEY,
|
||||||
|
__('Could not connect to license server.', 'wc-licensed-product') . ' ' . $e->getMessage(),
|
||||||
|
self::ERROR_CACHE_TTL
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't cache validation failure on network errors - allow retry on next page load
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last error message
|
||||||
|
*/
|
||||||
|
public function getLastError(): ?string
|
||||||
|
{
|
||||||
|
$error = get_transient(self::ERROR_CACHE_KEY);
|
||||||
|
return $error !== false ? (string) $error : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the validation cache
|
||||||
|
*/
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
delete_transient(self::CACHE_KEY);
|
||||||
|
delete_transient(self::ERROR_CACHE_KEY);
|
||||||
|
$this->isLocalhostCached = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running on localhost
|
||||||
|
*
|
||||||
|
* Matches localhost, 127.0.0.1, ::1, and any port number.
|
||||||
|
*/
|
||||||
|
public function isLocalhost(): bool
|
||||||
|
{
|
||||||
|
if ($this->isLocalhostCached !== null) {
|
||||||
|
return $this->isLocalhostCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain = $this->getCurrentDomain();
|
||||||
|
|
||||||
|
// Remove port number if present
|
||||||
|
$domainWithoutPort = preg_replace('/:[\d]+$/', '', $domain);
|
||||||
|
|
||||||
|
// Check for localhost variants
|
||||||
|
$localhostNames = ['localhost', '127.0.0.1', '::1'];
|
||||||
|
|
||||||
|
if (in_array($domainWithoutPort, $localhostNames, true)) {
|
||||||
|
$this->isLocalhostCached = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for .localhost and .local subdomains
|
||||||
|
if (
|
||||||
|
str_ends_with($domainWithoutPort, '.localhost') ||
|
||||||
|
str_ends_with($domainWithoutPort, '.local')
|
||||||
|
) {
|
||||||
|
$this->isLocalhostCached = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->isLocalhostCached = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current domain from the site URL
|
||||||
|
*/
|
||||||
|
private function getCurrentDomain(): string
|
||||||
|
{
|
||||||
|
$siteUrl = get_site_url();
|
||||||
|
$parsed = parse_url($siteUrl);
|
||||||
|
$host = $parsed['host'] ?? 'localhost';
|
||||||
|
|
||||||
|
// Include port if non-standard
|
||||||
|
if (isset($parsed['port'])) {
|
||||||
|
$host .= ':' . $parsed['port'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the license server URL from settings
|
||||||
|
*/
|
||||||
|
private function getLicenseServerUrl(): string
|
||||||
|
{
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_server_url', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the license key from settings
|
||||||
|
*/
|
||||||
|
private function getLicenseKey(): string
|
||||||
|
{
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server secret from settings (optional)
|
||||||
|
*/
|
||||||
|
private function getServerSecret(): ?string
|
||||||
|
{
|
||||||
|
$secret = get_option('wc_licensed_product_plugin_license_server_secret', '');
|
||||||
|
return !empty($secret) ? (string) $secret : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the license client instance
|
||||||
|
*/
|
||||||
|
private function createLicenseClient(): LicenseClientInterface
|
||||||
|
{
|
||||||
|
$httpClient = HttpClient::create([
|
||||||
|
'timeout' => 10,
|
||||||
|
'verify_peer' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serverUrl = $this->getLicenseServerUrl();
|
||||||
|
$serverSecret = $this->getServerSecret();
|
||||||
|
|
||||||
|
// Use secure client if server secret is configured
|
||||||
|
if ($serverSecret !== null) {
|
||||||
|
return new SecureLicenseClient(
|
||||||
|
httpClient: $httpClient,
|
||||||
|
baseUrl: $serverUrl,
|
||||||
|
serverSecret: $serverSecret,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LicenseClient(
|
||||||
|
httpClient: $httpClient,
|
||||||
|
baseUrl: $serverUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ declare(strict_types=1);
|
|||||||
namespace Jeremias\WcLicensedProduct;
|
namespace Jeremias\WcLicensedProduct;
|
||||||
|
|
||||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||||
|
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;
|
||||||
@@ -22,6 +24,7 @@ use Jeremias\WcLicensedProduct\Email\LicenseEmailController;
|
|||||||
use Jeremias\WcLicensedProduct\Frontend\AccountController;
|
use Jeremias\WcLicensedProduct\Frontend\AccountController;
|
||||||
use Jeremias\WcLicensedProduct\Frontend\DownloadController;
|
use Jeremias\WcLicensedProduct\Frontend\DownloadController;
|
||||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||||
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
||||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -96,6 +99,7 @@ final class Plugin
|
|||||||
$this->twig = new Environment($loader, [
|
$this->twig = new Environment($loader, [
|
||||||
'cache' => WP_CONTENT_DIR . '/cache/wc-licensed-product/twig',
|
'cache' => WP_CONTENT_DIR . '/cache/wc-licensed-product/twig',
|
||||||
'auto_reload' => true, // Always check for template changes
|
'auto_reload' => true, // Always check for template changes
|
||||||
|
'autoescape' => 'html', // Explicitly enable HTML autoescape for XSS protection
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add WordPress functions as Twig functions
|
// Add WordPress functions as Twig functions
|
||||||
@@ -119,13 +123,23 @@ final class Plugin
|
|||||||
$this->licenseManager = new LicenseManager();
|
$this->licenseManager = new LicenseManager();
|
||||||
$this->versionManager = new VersionManager();
|
$this->versionManager = new VersionManager();
|
||||||
|
|
||||||
// Initialize controllers
|
// Check plugin license
|
||||||
|
$licenseChecker = PluginLicenseChecker::getInstance();
|
||||||
|
$isLicensed = $licenseChecker->isLicenseValid();
|
||||||
|
|
||||||
|
// Always initialize product type (needed for existing orders)
|
||||||
new LicensedProductType();
|
new LicensedProductType();
|
||||||
new CheckoutController($this->licenseManager);
|
|
||||||
new StoreApiExtension($this->licenseManager);
|
// Only initialize frontend components if licensed or on localhost
|
||||||
$this->registerCheckoutBlocksIntegration();
|
if ($isLicensed) {
|
||||||
$this->downloadController = new DownloadController($this->licenseManager, $this->versionManager);
|
new CheckoutController($this->licenseManager);
|
||||||
new AccountController($this->twig, $this->licenseManager, $this->versionManager, $this->downloadController);
|
new StoreApiExtension($this->licenseManager);
|
||||||
|
$this->registerCheckoutBlocksIntegration();
|
||||||
|
$this->downloadController = new DownloadController($this->licenseManager, $this->versionManager);
|
||||||
|
new AccountController($this->twig, $this->licenseManager, $this->versionManager, $this->downloadController);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always initialize REST API and email controller
|
||||||
new RestApiController($this->licenseManager);
|
new RestApiController($this->licenseManager);
|
||||||
new LicenseEmailController($this->licenseManager);
|
new LicenseEmailController($this->licenseManager);
|
||||||
|
|
||||||
@@ -134,11 +148,19 @@ final class Plugin
|
|||||||
(new ResponseSigner())->register();
|
(new ResponseSigner())->register();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin always available
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
new AdminController($this->twig, $this->licenseManager);
|
new AdminController($this->twig, $this->licenseManager);
|
||||||
new VersionAdminController($this->versionManager);
|
new VersionAdminController($this->versionManager);
|
||||||
new OrderLicenseController($this->licenseManager);
|
new OrderLicenseController($this->licenseManager);
|
||||||
new SettingsController();
|
new SettingsController();
|
||||||
|
new DashboardWidgetController($this->licenseManager);
|
||||||
|
new DownloadWidgetController($this->versionManager);
|
||||||
|
|
||||||
|
// Show admin notice if unlicensed and not on localhost
|
||||||
|
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
|
||||||
|
add_action('admin_notices', [$this, 'showUnlicensedNotice']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,12 +186,16 @@ final class Plugin
|
|||||||
*/
|
*/
|
||||||
private function registerHooks(): void
|
private function registerHooks(): void
|
||||||
{
|
{
|
||||||
// Generate license on order completion (multiple hooks for compatibility)
|
// Only register order hooks if licensed (license generation requires valid license)
|
||||||
add_action('woocommerce_order_status_completed', [$this, 'onOrderCompleted']);
|
$licenseChecker = PluginLicenseChecker::getInstance();
|
||||||
add_action('woocommerce_order_status_processing', [$this, 'onOrderCompleted']);
|
if ($licenseChecker->isLicenseValid()) {
|
||||||
|
// Generate license on order completion (multiple hooks for compatibility)
|
||||||
|
add_action('woocommerce_order_status_completed', [$this, 'onOrderCompleted']);
|
||||||
|
add_action('woocommerce_order_status_processing', [$this, 'onOrderCompleted']);
|
||||||
|
|
||||||
// Also hook into payment complete for immediate license generation
|
// Also hook into payment complete for immediate license generation
|
||||||
add_action('woocommerce_payment_complete', [$this, 'onOrderCompleted']);
|
add_action('woocommerce_payment_complete', [$this, 'onOrderCompleted']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,4 +247,29 @@ final class Plugin
|
|||||||
{
|
{
|
||||||
return $this->twig->render($template, $context);
|
return $this->twig->render($template, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show admin notice when plugin is unlicensed
|
||||||
|
*/
|
||||||
|
public function showUnlicensedNotice(): void
|
||||||
|
{
|
||||||
|
$settingsUrl = admin_url('admin.php?page=wc-settings&tab=licensed_product');
|
||||||
|
?>
|
||||||
|
<div class="notice notice-warning is-dismissible">
|
||||||
|
<p>
|
||||||
|
<strong><?php esc_html_e('WC Licensed Product', 'wc-licensed-product'); ?>:</strong>
|
||||||
|
<?php esc_html_e('Plugin license is not configured or invalid. Frontend features are disabled.', 'wc-licensed-product'); ?>
|
||||||
|
<a href="<?php echo esc_url($settingsUrl); ?>"><?php esc_html_e('Configure License', 'wc-licensed-product'); ?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin license checker instance
|
||||||
|
*/
|
||||||
|
public function getLicenseChecker(): PluginLicenseChecker
|
||||||
|
{
|
||||||
|
return PluginLicenseChecker::getInstance();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ final class LicensedProductType
|
|||||||
|
|
||||||
// Make product virtual by default
|
// Make product virtual by default
|
||||||
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
|
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
|
||||||
|
|
||||||
|
// Display current version under product title on single product page
|
||||||
|
add_action('woocommerce_single_product_summary', [$this, 'displayCurrentVersion'], 6);
|
||||||
|
|
||||||
|
// Enqueue frontend CSS for licensed products on single product pages
|
||||||
|
add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,4 +241,52 @@ final class LicensedProductType
|
|||||||
}
|
}
|
||||||
return $isVirtual;
|
return $isVirtual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend styles for licensed products on single product pages
|
||||||
|
*/
|
||||||
|
public function enqueueFrontendStyles(): void
|
||||||
|
{
|
||||||
|
if (!is_product()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product || !$product->is_type('licensed')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'wc-licensed-product-frontend',
|
||||||
|
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/frontend.css',
|
||||||
|
[],
|
||||||
|
WC_LICENSED_PRODUCT_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display current version under product title on single product page
|
||||||
|
*/
|
||||||
|
public function displayCurrentVersion(): void
|
||||||
|
{
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product || !$product->is_type('licensed')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var LicensedProduct $product */
|
||||||
|
$version = $product->get_current_version();
|
||||||
|
|
||||||
|
if (empty($version)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<p class="wclp-product-version"><span class="version-label">%s</span> <span class="version-number">%s</span></p>',
|
||||||
|
esc_html__('Version:', 'wc-licensed-product'),
|
||||||
|
esc_html($version)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1 class="wp-heading-inline">{{ __('Licenses') }}</h1>
|
<h1 class="wp-heading-inline">{{ __('Licenses') }}</h1>
|
||||||
<a href="{{ admin_url }}?action=export_csv" class="page-title-action">
|
<a href="{{ export_csv_url() }}" class="page-title-action">
|
||||||
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
|
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
|
||||||
{{ __('Export CSV') }}
|
{{ __('Export CSV') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -143,8 +143,8 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="wclp-editable-cell" data-field="status" data-license-id="{{ item.license.id }}">
|
<td class="wclp-editable-cell" data-field="status" data-license-id="{{ item.license.id }}">
|
||||||
<span class="wclp-display-value">
|
<span class="wclp-display-value">
|
||||||
<span class="license-status license-status-{{ item.license.status }}">
|
<span class="license-status license-status-{{ esc_attr(item.license.status) }}">
|
||||||
{{ item.license.status|capitalize }}
|
{{ esc_html(item.license.status)|capitalize }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
|
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
|
||||||
@@ -184,6 +184,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="license-actions">
|
<td class="license-actions">
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
|
<span class="test">
|
||||||
|
<a href="#" class="wclp-test-license-link"
|
||||||
|
data-license-id="{{ item.license.id }}"
|
||||||
|
data-license-key="{{ esc_attr(item.license.licenseKey) }}"
|
||||||
|
data-domain="{{ esc_attr(item.license.domain) }}"
|
||||||
|
title="{{ __('Test license against API') }}">{{ __('Test') }}</a> |
|
||||||
|
</span>
|
||||||
{% if item.license.status != 'revoked' %}
|
{% if item.license.status != 'revoked' %}
|
||||||
<span class="transfer">
|
<span class="transfer">
|
||||||
<a href="#" class="wclp-transfer-link"
|
<a href="#" class="wclp-transfer-link"
|
||||||
@@ -272,6 +279,36 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Test License Modal -->
|
||||||
|
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
|
||||||
|
<div class="wclp-modal-content">
|
||||||
|
<span class="wclp-modal-close">×</span>
|
||||||
|
<h2>{{ __('License Validation Test') }}</h2>
|
||||||
|
<div class="wclp-test-info">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ __('License Key') }}</th>
|
||||||
|
<td><code id="test-license-key"></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{ __('Domain') }}</th>
|
||||||
|
<td><code id="test-domain"></code></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
|
||||||
|
<span class="spinner is-active" style="float:none;"></span>
|
||||||
|
<p>{{ __('Testing license...') }}</p>
|
||||||
|
</div>
|
||||||
|
<div id="wclp-test-result" style="display:none;">
|
||||||
|
<div id="wclp-test-result-content"></div>
|
||||||
|
</div>
|
||||||
|
<p class="submit">
|
||||||
|
<button type="button" class="button wclp-modal-cancel">{{ __('Close') }}</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transfer Modal -->
|
<!-- Transfer Modal -->
|
||||||
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
|
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
|
||||||
<div class="wclp-modal-content">
|
<div class="wclp-modal-content">
|
||||||
@@ -349,5 +386,91 @@
|
|||||||
$modal.hide();
|
$modal.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test License modal
|
||||||
|
var $testModal = $('#wclp-test-modal');
|
||||||
|
var $testLoading = $('#wclp-test-loading');
|
||||||
|
var $testResult = $('#wclp-test-result');
|
||||||
|
var $testResultContent = $('#wclp-test-result-content');
|
||||||
|
|
||||||
|
$('.wclp-test-license-link').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var licenseKey = $(this).data('license-key');
|
||||||
|
var domain = $(this).data('domain');
|
||||||
|
|
||||||
|
// Show modal with info
|
||||||
|
$('#test-license-key').text(licenseKey);
|
||||||
|
$('#test-domain').text(domain);
|
||||||
|
$testLoading.show();
|
||||||
|
$testResult.hide();
|
||||||
|
$testModal.show();
|
||||||
|
|
||||||
|
// Call the test endpoint
|
||||||
|
$.ajax({
|
||||||
|
url: wclpAdmin.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wclp_test_license',
|
||||||
|
nonce: wclpAdmin.editNonce,
|
||||||
|
license_key: licenseKey,
|
||||||
|
domain: domain
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
$testLoading.hide();
|
||||||
|
if (response.success) {
|
||||||
|
var result = response.data;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
html = '<div class="notice notice-success inline"><p><strong>✓ {{ __('License is VALID') }}</strong></p></div>';
|
||||||
|
html += '<table class="widefat striped"><tbody>';
|
||||||
|
html += '<tr><th>{{ __('Product') }}</th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
|
||||||
|
html += '<tr><th>{{ __('Version') }}</th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
|
||||||
|
if (result.expires_at) {
|
||||||
|
html += '<tr><th>{{ __('Expires') }}</th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
||||||
|
} else {
|
||||||
|
html += '<tr><th>{{ __('Expires') }}</th><td>{{ __('Lifetime') }}</td></tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html = '<div class="notice notice-error inline"><p><strong>✗ {{ __('License is INVALID') }}</strong></p></div>';
|
||||||
|
html += '<table class="widefat striped"><tbody>';
|
||||||
|
html += '<tr><th>{{ __('Error Code') }}</th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
|
||||||
|
html += '<tr><th>{{ __('Message') }}</th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
|
||||||
|
html += '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$testResultContent.html(html);
|
||||||
|
$testResult.show();
|
||||||
|
} else {
|
||||||
|
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
|
||||||
|
$testResult.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$testLoading.hide();
|
||||||
|
$testResultContent.html('<div class="notice notice-error inline"><p>{{ __('Failed to test license. Please try again.') }}</p></div>');
|
||||||
|
$testResult.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close test modal
|
||||||
|
$testModal.find('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
|
||||||
|
$testModal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on('click', function(e) {
|
||||||
|
if ($(e.target).is($testModal)) {
|
||||||
|
$testModal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
{{ esc_html(item.product_name) }}
|
{{ esc_html(item.product_name) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
<span class="license-status license-status-{{ item.license.status }}">
|
<span class="license-status license-status-{{ esc_attr(item.license.status) }}">
|
||||||
{{ item.license.status|capitalize }}
|
{{ esc_html(item.license.status)|capitalize }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,13 +57,22 @@
|
|||||||
<h4>{{ __('Available Downloads') }}</h4>
|
<h4>{{ __('Available Downloads') }}</h4>
|
||||||
<ul class="download-list">
|
<ul class="download-list">
|
||||||
{% for download in item.downloads %}
|
{% for download in item.downloads %}
|
||||||
<li>
|
<li class="download-item">
|
||||||
<a href="{{ esc_url(download.download_url) }}" class="download-link">
|
<div class="download-row-file">
|
||||||
<span class="dashicons dashicons-download"></span>
|
<a href="{{ esc_url(download.download_url) }}" class="download-link">
|
||||||
{{ esc_html(download.filename ?: 'Version ' ~ download.version) }}
|
<span class="dashicons dashicons-download"></span>
|
||||||
</a>
|
{{ esc_html(download.filename ?: 'Version ' ~ download.version) }}
|
||||||
<span class="download-version">v{{ esc_html(download.version) }}</span>
|
</a>
|
||||||
<span class="download-date">{{ esc_html(download.released_at) }}</span>
|
</div>
|
||||||
|
<div class="download-row-meta">
|
||||||
|
<span class="download-date">{{ esc_html(download.released_at) }}</span>
|
||||||
|
{% if download.file_hash %}
|
||||||
|
<span class="download-hash" title="{{ esc_attr(download.file_hash) }}">
|
||||||
|
<span class="dashicons dashicons-shield"></span>
|
||||||
|
<code>{{ download.file_hash[:12] }}...</code>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -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.2.1
|
* Version: 0.3.7
|
||||||
* 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.2.1');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.3.7');
|
||||||
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