46 KiB
WooCommerce Plugin for licensed Products
Author: Marco Graetsch Author URL: https://src.bundespruefstelle.ch/magdev Author Email: magdev3.0@gmail.com Repository URL: https://src.bundespruefstelle.ch/magdev/wc-licensed-product Issues URL: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues
Project Overview
This plugin adds a new product type named licensed to sell software products using license keys. It implements an endpoint to check valid licenses against the WooCommerce payment database. License keys are generated are bounded to a specific domain, which must be defined within the checkout process, and will be available as soon as an order is marked as paid. License keys can optionally be bound to major software versions. Customer can view their purchased license keys in their customer profile.
Features
- Add a new product type named
licensed - Generate license keys based on purchased releases, bounded to a specific domain, if the related payment is marked as completed
Frontend
- Customers can view their purchased licenses in the customer area
Administration
- Add CRUD for license keys
- Manage different versions of the same licensed product
Key Fact: 100% AI-Generated
This project is proudly "vibe-coded" using Claude.AI - the entire codebase was created through AI assistance.
Temporary Roadmap
Note for AI Assistants: Clean this section after the specific features are done or new releases are made. Effective changes are tracked in CHANGELOG.md. Do not add completed versions here - document them in the Session History section at the end of this file.
Known Bugs
- Take a look at the logfile in
tmp/
Version 0.3.9
No changes at the moment.
Version 0.4.0
No changes at the moment.
Technical Stack
- Language: PHP 8.3.x
- Framework: Latest WordPress Plugin API
- E-commerce: WooCommerce 10.0+
- Template Engine: Twig 3.0 (via Composer)
- Wordpress Base Theme twentytwentyfive
- Frontend: Vanilla JavaScript
- Styling: Custom CSS
- Dependency Management: Composer
- Internationalization: WordPress i18n (.pot/.po/.mo files)
- Canonical Plugin Name:
wc-licensed-product
Security Best Practices
- All user inputs are sanitized (integers for quantities/prices)
- Nonce verification on form submissions
- Output escaping in templates (
esc_attr,esc_html,esc_js) - Direct file access prevention via
ABSPATHcheck
Translation Ready
All user-facing strings use:
__('Text to translate', 'wc-licensed-product')
_e('Text to translate', 'wc-licensed-product')
Text domain: wc-licensed-product
Translation Template
- Base
.potfile created:languages/wc-licensed-product.pot - Ready for translation to any locale
- All translatable strings properly marked with text domain
Available Translations
en_US- English (United States) [base language - .pot template]de_CH- German (Switzerland, formal)
To compile translations to .mo files for production:
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
Create releases
- The
vendor/directory MUST be included in releases (Dependencies required for runtime) - Don't create any release files until version 0.1.x and up!
- CRITICAL: Build
vendor/for the MINIMUM supported PHP version, not the development version- Use
composer config platform.php 8.3.0before building release packages - Run
composer update --no-dev --optimize-autoloaderto rebuild dependencies
- Use
- CRITICAL: WordPress requires plugins in a subdirectory structure
- Run zip from the
plugins/parent directory, NOT from within the plugin directory - Package must extract to
wc-licensed-product/subdirectory with main file atwc-licensed-product/wc-licensed-product.php - Correct command:
cd /wp-content/plugins/ && zip -r wc-licensed-product/releases/wc-licensed-product-x.x.x.zip wc-licensed-product ... - Wrong: Running zip from inside the plugin directory creates files at root level
- Run zip from the
- CRITICAL: Exclude symlinks explicitly - zip follows symlinks by default
- Always use
-x "wc-licensed-product/wp-core" -x "wc-licensed-product/wp-core/*" -x "wc-licensed-product/wp-plugins" -x "wc-licensed-product/wp-plugins/*"to exclude development symlinks - Otherwise the entire linked directory contents will be included in the package
- Always use
- Exclusion patterns must match the relative path structure used in zip command
- Always verify the package structure with
unzip -lbefore distribution- Check all files are prefixed with
wc-licensed-product/ - Verify main file is at
wc-licensed-product/wc-licensed-product.php - Check for duplicate entries (indicates multiple builds in same archive)
- Check all files are prefixed with
- Test installation on the minimum supported PHP version before final deployment
- Releases are stored in
releases/including checksums - Track release changes in a single
CHANGELOG.mdfile - Bump the version number to either bugfix release versions or on new features minor release versions
- CRITICAL: WordPress reads version from TWO places - BOTH must be updated:
- Plugin header comment
Version: x.x.x(line ~6 in wc-licensed-product.php) - WordPress uses THIS for admin display - PHP constant
WC_LICENSED_PRODUCT_VERSION(line ~28) - Used internally by the plugin
- If only the constant is updated, WordPress will show the old version in Plugins list
- Plugin header comment
Important Git Notes:
- Default branch while development is
dev - Create releases from branch
mainafter merging branchdev - Tags should use format
vX.X.X(e.g.,v1.1.22), start with v0.1.0 - Use annotated tags (
-a) not lightweight tags - Commit messages should follow the established format with Claude Code attribution
.claude/settings.local.jsonchanges are typically local-only (stash before rebasing)
What Gets Released
- All plugin source files
- Compiled vendor dependencies
- Translation files (.mo compiled from .po)
- Assets (CSS, JS)
- Documentation (README, CHANGELOG, etc.)
What's Excluded
- Git metadata (
.git/) - Development files (
.vscode/,.claude/,CLAUDE.md,wp-core,wp-plugins) - Logs and cache files
- Previous releases
composer.lock(butvendor/is included)
For AI Assistants:
When starting a new session on this project:
- Read this CLAUDE.md file first
- Semantic versioning follows the
MAJOR.MINOR.BUGFIXpattern - Check git log for recent changes
- Verify you're on the
devbranch before making changes - Run
composer installif vendor/ is missing - Test changes before committing
- Follow commit message format with Claude Code attribution
- Update this session history section with learnings
- Never commit backup files (
*.po~,*.bak, etc.) - checkgit statusbefore committing - Follow markdown linting rules (see below)
Always refer to this document when starting work on this project.
Markdown Linting Rules
When editing CLAUDE.md or other markdown files, follow these rules to avoid linting errors:
-
MD031 - Blank lines around fenced code blocks: Always add a blank line before and after fenced code blocks, even when they follow list items. Example of correct format:
-
Item label:
(blank line here) ```php code example ``` (blank line here)
-
-
MD056 - Table column count: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.
-
MD009 - No trailing spaces: Remove trailing whitespace from lines
-
MD012 - No multiple consecutive blank lines: Use only single blank lines between sections
-
MD040 - Fenced code blocks should have a language specified: Always add a language identifier to code blocks (e.g.,
txt,bash,php). For shortcode examples, usetxt. -
MD032 - Lists should be surrounded by blank lines: Add a blank line before AND after list blocks, including after bold labels like
**Attributes:**. -
MD034 - Bare URLs: Wrap URLs in angle brackets (e.g.,
<https://example.com>) or use markdown link syntax[text](url). -
Author section formatting: Use a heading (
### Name) instead of bold (**Name**) for the author name to maintain consistent document structure.
Project Architecture
Directory Structure
wc-licensed-product/
├── assets/
│ └── css/ # Frontend and admin stylesheets
├── languages/ # Translation files (.pot, .po, .mo)
├── releases/ # Release packages (version 0.1.0+)
├── src/
│ ├── Admin/ # AdminController - license management UI
│ ├── Api/ # RestApiController - license validation endpoints
│ ├── Checkout/ # CheckoutController - domain field at checkout
│ ├── Frontend/ # AccountController - customer licenses page
│ ├── License/ # License model and LicenseManager
│ └── Product/ # LicensedProduct type and LicensedProductType
├── templates/
│ ├── admin/ # Twig templates for admin views
│ └── frontend/ # Twig templates for customer views
└── vendor/ # Composer dependencies (Twig)
Database Tables
Created on plugin activation via Installer::createTables():
{prefix}_wc_licensed_product_licenses- License keys with domain binding{prefix}_wc_licensed_product_versions- Product version tracking
REST API Endpoints
Base: /wp-json/wc-licensed-product/v1/
| Endpoint | Method | Description |
|---|---|---|
/validate |
POST | Validate license key for domain |
/status |
POST | Get license status |
/activate |
POST | Activate license on domain |
Full API documentation available in openapi.json (OpenAPI 3.1 specification).
Key Classes
Plugin(singleton) - Main controller, initializes all componentsInstaller- Database setup, activation/deactivation hooksLicenseManager- License CRUD, validation, key generationLicense- Entity model with status constantsLicensedProduct- Extends WC_Product for licensed productsLicensedProductType- Registers product type with WooCommerce
Session History
2026-01-21 - Initial Implementation (v0.0.1)
Implemented:
- Created basic WordPress/WooCommerce plugin structure
- Implemented domain-bound license management via REST API
- Added "Licensed Product" WooCommerce product type
- License generation on order completion (XXXX-XXXX-XXXX-XXXX format)
- Customer account page for viewing licenses
- Admin CRUD interface for license management
- Checkout domain field for licensed products
- German (de_CH) translation
Architecture decisions:
- Used Twig 3.0 for templating with fallback PHP templates
- License keys use format XXXX-XXXX-XXXX-XXXX (alphanumeric)
- Licenses bound to normalized domains (no protocol, no www, lowercase)
- REST API at
/wp-json/wc-licensed-product/v1/ - Custom database tables for licenses and product versions
- WooCommerce HPOS compatibility included
2026-01-21 - Version 0.0.2 Features
Implemented:
- Product version management UI (meta box on product edit page)
- AJAX-based version CRUD operations
- Email notifications with license keys on order completion
- REST API rate limiting (30 requests/minute per IP)
New classes:
ProductVersion- Version entity modelVersionManager- Version CRUD operationsVersionAdminController- Admin UI for versionsLicenseEmailController- Email integration
Technical notes:
- Rate limiting uses WordPress transients
- IP detection supports Cloudflare and proxies
- Version management uses AJAX for smooth UX
2026-01-21 - Version 0.0.3 Features
Implemented:
- File attachment support for product versions (WordPress Media Library integration)
- Version auto-detection from uploaded filenames (e.g.,
plugin-v1.2.3.zip) - Customer download page for purchased licenses with secure authenticated downloads
- License key copy-to-clipboard functionality on account page
- New card-based UI for customer licenses page with download section
- Product versions meta box visibility fix (now hidden for non-licensed product types)
New classes:
DownloadController- Secure file delivery with hash-based URL verification
Technical notes:
- Secure download URLs use hash verification (license_id-version_id-hash format)
- Database schema updated:
attachment_idcolumn added to versions table - ProductVersion model extended with
getEffectiveDownloadUrl()andgetDownloadFilename() - Media uploader filters for zip files only
- Clipboard API with fallback for older browsers
- Card-based responsive UI design for licenses page
Bug fixes:
- Fixed product versions meta box visibility for non-licensed product types (targets
#wc_licensed_product_versionscontainer)
2026-01-21 - Version 0.0.4 Features
Implemented:
- WooCommerce settings tab "Licensed Products" for default license settings
- Default settings for Max Activations, License Validity, and Bind to Major Version
- Per-product settings override global defaults (empty = use default)
- Settings link in product edit page pointing to WooCommerce settings
New classes:
SettingsController- WooCommerce settings tab integration
Technical notes:
- Settings stored using WooCommerce options API
- LicensedProduct model falls back to global defaults when product settings are empty
- Added
has_custom_*methods to check if product has overridden defaults - Product edit panel shows placeholders with default values
2026-01-21 - Version 0.0.5 Features
Implemented:
- Bulk license operations in admin (activate, deactivate, revoke, extend, delete)
- License renewal/extension functionality (extend by 30/90/365 days)
- Set license to lifetime (remove expiration)
- Quick action buttons per license row (+30d, ∞, Revoke, Delete)
- Checkbox selection with select-all for bulk operations
- Improved admin notices for bulk operation results
New methods in LicenseManager:
extendLicense()- Extend license by specified dayssetLicenseLifetime()- Remove expiration (set to lifetime)bulkUpdateStatus()- Bulk update license statusbulkDelete()- Bulk delete licensesbulkExtend()- Bulk extend licenses
Technical notes:
- Admin page redesigned with WordPress list table styling
- Twig template functions for generating action URLs with nonces
- JavaScript for checkbox synchronization and bulk action handling
- Row-actions pattern for cleaner per-license action UI
2026-01-21 - Version 0.0.6 Features
Implemented:
- License usage statistics/analytics dashboard (WooCommerce > License Dashboard)
- License transfer functionality (change domain from admin)
- Export licenses to CSV with all related data
- OpenAPI 3.1 specification for REST API (
openapi.json) - Removed
/deactivateAPI endpoint (admin-only operation now)
New methods in LicenseManager:
transferLicense()- Transfer license to new domaingetStatistics()- Get comprehensive license statisticsexportLicensesForCsv()- Export all licenses for CSV download
New files:
templates/admin/dashboard.html.twig- Dashboard templateopenapi.json- OpenAPI 3.1 specification
Technical notes:
- Dashboard shows status cards, monthly chart, top products/domains
- Transfer uses modal dialog with form submission
- CSV export includes BOM for UTF-8 Excel compatibility
- OpenAPI spec documents all endpoints with examples and error responses
- Statistics query uses SQL aggregation for performance
2026-01-21 - Version 0.0.7 Features
Implemented:
- License Dashboard moved to WooCommerce Reports section (Reports > Licenses tab)
- License search and filtering in admin (by key, domain, status, product)
- Customer-facing license transfer request with AJAX modal
- Email notifications for license expiration warnings (7 days and 1 day before)
- Bulk import licenses from CSV with validation and options
New methods in LicenseManager:
getLicensesExpiringSoon()- Get licenses expiring within specified daysmarkExpirationNotified()- Track expiration notification statewasExpirationNotified()- Check if notification already sentimportLicense()- Create license from imported data
Updated controllers:
AdminController- Added Reports integration, search/filter handling, CSV importAccountController- Added customer transfer AJAX handler with domain validationLicenseEmailController- Added cron scheduling and expiration warning emailsInstaller- Clears cron events on deactivation
New UI features:
- Search box and filter dropdowns on licenses page
- Transfer button and modal on customer licenses page
- Import CSV page with format documentation
- Pagination preserves filter state
Technical notes:
- WooCommerce Reports integration uses
woocommerce_admin_reportsfilter - Customer transfer uses AJAX with nonce verification
- Expiration warnings use WordPress cron with daily schedule
- CSV import supports both exported format and simplified format
- User meta tracks expiration notifications to prevent duplicates
2026-01-21 - Version 0.0.8 Features
Implemented:
- Removed "Current Version" field from product license settings
- Current version now automatically derived from latest product version in database
- Refactored email system to use WooCommerce transactional email system
- License Expiration Warning email configurable via WooCommerce > Settings > Emails
- Uses WooCommerce's email header/footer templates for consistent styling
- Configurable warning days for expiration notifications (first and second warning)
New classes:
LicenseExpirationEmail- WooCommerce WC_Email subclass for license expiration warnings
Modified classes:
LicensedProduct-get_current_version()andget_major_version()now query VersionManagerLicensedProductType- Removed Current Version field from product data panelVersionAdminController- Removed automatic update of_licensed_current_versionmetaLicenseEmailController- Registers WooCommerce email class, uses WC email system for sendingSettingsController- Updated email section to link to WooCommerce email settings
Technical notes:
- Email uses WooCommerce's
emails/email-header.phpandemails/email-footer.phptemplates - Email content is rendered inline, inheriting WooCommerce's styling and customizations
- Admins can customize subject, heading, additional content, and enable/disable via WC settings
- Expiration warning schedule (days before) remains in plugin settings
- Email enable/disable is controlled through WooCommerce email settings
2026-01-21 - Version 0.0.9 Features
Implemented:
- Created API client examples for multiple programming languages
- Documentation for REST API usage with code examples
New files:
docs/client-examples/README.md- API documentation and overviewdocs/client-examples/curl.sh- cURL command examplesdocs/client-examples/php-client.php- PHP client class with examplesdocs/client-examples/python-client.py- Python client class with examplesdocs/client-examples/javascript-client.js- JavaScript/Node.js client classdocs/client-examples/csharp-client.cs- C# client class with examples
Technical notes:
- All client examples include error handling for rate limiting (HTTP 429)
- Examples demonstrate all three API endpoints: validate, status, activate
- JavaScript client works in both browser and Node.js environments
- Python client uses dataclasses for type-safe responses
- C# client uses async/await patterns and System.Text.Json
2026-01-21 - Bug Fixes (pre-v0.0.10)
Fixed bugs:
- Fixed: No licenses generated when order is marked as done
- Fixed: No domain field in WooCommerce Checkout Blocks
New files:
src/Checkout/CheckoutBlocksIntegration.php- WooCommerce Blocks checkout integrationsrc/Checkout/StoreApiExtension.php- Store API extension for domain field handlingassets/js/checkout-blocks.js- Frontend JavaScript for checkout block domain field
Modified files:
src/Plugin.php- Added checkout blocks registration and multiple order status hookssrc/License/LicenseManager.php- Fixed product type check ingenerateLicense()
Technical notes:
- Added support for WooCommerce Checkout Blocks (default since WC 8.3+)
CheckoutBlocksIntegrationimplementsIntegrationInterfacefor block checkoutStoreApiExtensionhandles server-side domain data via Store API- License generation now triggers on
completed,processing, andpayment_completehooks - Fixed
LicenseManager::generateLicense()to properly check product type withis_type() - Classic checkout (
woocommerce_after_order_notes) still works for stores using classic checkout
Additional bug fix:
- Fixed: Version uploads not appearing in list after creation
Modified files:
src/Product/VersionManager.php- Fixed nullattachment_idhandling increateVersion()src/Admin/VersionAdminController.php- Added product validation and error logging
Technical notes:
$wpdb->insert()now only includesattachment_idfield when it has a valid value- Added error logging for version creation failures to aid debugging
- Improved meta box visibility logic for new products
2026-01-21 - Version 0.0.10 Features
Implemented:
- License meta box on WooCommerce order edit pages
- Editable order domain field with AJAX-based inline editing
- Editable license domains directly from the order page
- Licenses table showing all licenses associated with an order
- Support for both classic orders and HPOS (High-Performance Order Storage)
New files:
src/Admin/OrderLicenseController.php- Order page license integrationassets/js/order-licenses.js- JavaScript for inline domain editing
New methods in LicenseManager:
getLicensesByOrder()- Get all licenses for a specific order
Technical notes:
- Meta box automatically detects HPOS vs classic order storage
- AJAX handlers for updating both order domain and individual license domains
- Domain validation with normalization (strips protocol, www prefix)
- Inline edit UI with save/cancel for license domains
- Links to full licenses management page for advanced actions
Additional features added in this session:
- Inline editing for license fields (status, expiry date, domain) in admin licenses overview
- Copy license key button with clipboard API and fallback for older browsers
- Live search for licenses in admin overview with AJAX-powered results dropdown
- Settings link added to plugin action links in Plugins list
- Fixed 404 error on licenses menu item in customer account
- Fixed Twig template cache issues with
auto_reloadoption
New files:
assets/js/admin-licenses.js- JavaScript for live search, inline editing, and copy functionality
New methods in LicenseManager:
updateLicenseExpiry()- Update license expiry date with auto-reactivation for expired licenses
AJAX handlers added to AdminController:
handleAjaxStatusUpdate()- Update license status via AJAXhandleAjaxExpiryUpdate()- Update license expiry date via AJAXhandleAjaxDomainUpdate()- Update license domain via AJAXhandleAjaxRevoke()- Revoke license via AJAX
Technical notes:
- Live search uses 300ms debounce and keyboard navigation (arrows, enter, escape)
- Inline editing shows edit icons on hover, supports enter to save and escape to cancel
- Copy button uses Clipboard API with textarea fallback for older browsers
- All AJAX handlers use nonce verification (
wclp_inline_editnonce) - Twig configured with
auto_reload => truefor development to always check template changes
Bug fix:
- Fixed: Licenses menu item in customer account page resulted in 404 error
- Root cause: WooCommerce My Account endpoints require both
add_rewrite_endpoint()AND registration withwoocommerce_get_query_varsfilter - Fix: Added
addLicensesQueryVar()method to register the endpoint query var with WooCommerce
Release v0.0.10:
- Created release package:
releases/wc-licensed-product-0.0.10.zip(472 KB) - SHA256:
3f4a093f6d4d02389082c3a88c00542f477ab3ad4d4a0c65079e524ef0739620 - Tagged as
v0.0.10and pushed tomainbranch
2026-01-22 - Version 0.0.11 Features
Implemented:
- Created date column added to admin license overview showing when each license was generated
Modified files:
templates/admin/licenses.html.twig- Added "Created" column to table header and data cellssrc/Admin/AdminController.php- Added "Created" column to PHP fallback renderingsrc/Plugin.php- AddedgetInstance()alias for singleton access
Technical notes:
- New column displays license creation date in Y-m-d format
- Both Twig template and PHP fallback updated for consistency
- WooCommerce Analytics integration was attempted but removed due to WordPress permission issues with submenu pages
Release v0.0.11:
- Created release package:
releases/wc-licensed-product-0.0.11.zip(473 KB) - SHA256:
c3f66c4ac54741053f87ce1a63b4ddb49ad9707d5c194a271311bb95518ab13c - Tagged as
v0.0.11and pushed tomainbranch
2026-01-22 - Version 0.1.0 - First Stable Minor Release
Overview:
First stable minor release after comprehensive code review for WordPress/WooCommerce best practices and security.
Code Review Findings:
Security practices verified:
- Input sanitization with
sanitize_text_field(),absint(),esc_attr(),esc_html(),esc_url() - Nonce verification on all forms and AJAX handlers
- Capability checks with
current_user_can('manage_woocommerce') - SQL injection prevention using
$wpdb->prepare()throughout - Secure download URLs with hash verification using
hash_equals() - Rate limiting on REST API (30 requests/minute)
- Cryptographically secure license key generation with
random_int()
Bug Fixes:
- Fixed
VersionManager::updateVersion()null format handling for attachment ID updates - Improved input sanitization in
AdminController::enqueueAdminAssets()for page context checks
Documentation Updates:
- Updated README.md with complete feature documentation
- Added new features: Live Search, Inline Editing, Order Integration, WooCommerce HPOS compatibility, Checkout Blocks support
- Removed outdated "Current Version" field from usage instructions
Translation Updates:
- Regenerated .pot template with all current strings
- Updated German (de_CH) translation with new strings
- Compiled .mo file for production use
Modified files:
src/Product/VersionManager.php- Fixed null format handling in attachment updatesrc/Admin/AdminController.php- Improved $_GET sanitization for page contextREADME.md- Updated feature documentationCHANGELOG.md- Added 0.1.0 release noteswc-licensed-product.php- Version bump to 0.1.0languages/*- Updated all translation files
Release v0.1.0:
- Created release package:
releases/wc-licensed-product-0.1.0.zip(478 KB) - SHA256:
62638e240315107098be4cb40faff8395e9e1b719d79b73d80e69d680b305e87 - Tagged as
v0.1.0and pushed tomainbranch
2026-01-22 - Version 0.2.0 - Security & Integrity Features
Overview:
Added response signing for REST API security and SHA256 checksum validation for uploaded version files.
Implemented:
- REST API response signing using HMAC-SHA256 for tamper-proof responses
- SHA256 hash field for product version uploads with server-side validation
- Per-license key derivation using HKDF-like approach
- Automatic signature headers on license API endpoints
Removed:
- External download URL field from product version form
- Direct URL support in version uploads (Media Library only now)
New files:
src/Api/ResponseSigner.php- HMAC-SHA256 response signing class
Modified files:
src/Installer.php- Addedfile_hashcolumn to versions table schemasrc/Product/ProductVersion.php- AddedfileHashproperty and gettersrc/Product/VersionManager.php- Removed$downloadUrlparameter, added$fileHashwith validationsrc/Admin/VersionAdminController.php- Removed URL field, added SHA256 hash fieldassets/js/versions.js- Updated form handling for hash fieldsrc/Plugin.php- Initialize ResponseSigner when server secret is configured
Technical notes:
- Response signing only activates when
WC_LICENSE_SERVER_SECRETconstant is defined - Signature algorithm:
HMAC-SHA256(derived_key, timestamp + ':' + canonical_json) - Key derivation:
HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret) - Hash validation throws
InvalidArgumentExceptionon mismatch - Compatible with
magdev/wc-licensed-product-clientSecureLicenseClient - Database migration handled by WordPress
dbDelta()function
Configuration:
To enable response signing, add to wp-config.php:
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.0and pushed tomainbranch
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 hashassets/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
.sha256and.txtextensions - 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.1and pushed tomainbranch
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 rowssrc/Frontend/AccountController.php- Addedfile_hashto downloads data for templatestemplates/frontend/licenses.html.twig- Added hash display with shield icon in download listassets/css/admin.css- Added.file-hashstyles for admin tableassets/css/frontend.css- Added.download-hashstyles for customer downloadslanguages/*- Updated all translation files (304 strings)
Technical notes:
- Admin table shows hash in monospace
<code>element withcursor: help - Frontend shows green shield dashicon next to truncated hash
- Both use HTML
titleattribute 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.2and pushed tomainbranch
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-clientlibrary - 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- Addedmagdev/wc-licensed-product-clientdependencysrc/Admin/SettingsController.php- Added license settings fields and status displaysrc/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
LicenseClientorSecureLicenseClientbased 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-sectionslanguages/*- 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_productfor 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.1and pushed tomainbranch
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-clientfrom local path to git repository URL - Composer now fetches from:
https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git - Release package excludes vendor
.gitdirectories
Release v0.3.2:
- Created release package:
releases/wc-licensed-product-0.3.2.zip(810 KB) - SHA256:
ca33c81516b5dcf4a80b3192d8ae4ad39a7bf67196a1f729b563c5ae01b1d39c - Tagged as
v0.3.2and pushed tomainbranch
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)toupdateVersion($versionId, null, !$currentlyActive, null)
Implemented:
- Added "Test" action to license overview to validate licenses against
/validateAPI 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 methodsrc/Admin/AdminController.php- Added Test action to PHP fallback and AJAX handlertemplates/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.3and pushed tomainbranch
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- AddeddisplayCurrentVersion()andenqueueFrontendStyles()methodsassets/css/frontend.css- Added.wclp-product-versionstyles
Technical notes:
- Uses
woocommerce_single_product_summaryhook 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 queriesVersionManager::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 controllersrc/Email/LicenseExpiredEmail.php- WooCommerce email for expired license notifications
Modified files:
src/Plugin.php- Added DashboardWidgetController instantiationsrc/License/LicenseManager.php- AddedgetExpiredActiveLicenses()andautoExpireLicense()methodssrc/Email/LicenseEmailController.php- Added auto-expire logic and LicenseExpiredEmail registrationtemplates/frontend/licenses.html.twig- Restructured download list with two-row layoutassets/css/frontend.css- Added dashboard widget and download list styles
Technical notes:
- Dashboard widget uses
wp_add_dashboard_widget()hook, requiresmanage_woocommercecapability - Widget displays statistics from existing
LicenseManager::getStatistics()method - Auto-expire runs during daily
wclp_check_expiring_licensescron event getExpiredActiveLicenses()finds licenses with past expiration date but still active statusautoExpireLicense()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- AddedrecursiveKeySort()method for proper signature generationsrc/Api/RestApiController.php- Added trusted proxy support withisTrustedProxy(),isCloudflareIp(),ipMatchesCidr()methodssrc/Plugin.php- Added explicitautoescape => 'html'to Twig environmentsrc/Admin/AdminController.php- Added nonce verification tohandleCsvExport(), addedexport_csv_url()Twig functiontemplates/frontend/licenses.html.twig- Addedesc_attr()for CSS class statustemplates/admin/licenses.html.twig- Addedesc_attr()for CSS class status, updated export link to useexport_csv_url()
Configuration:
To enable trusted proxy support for rate limiting, add to wp-config.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) whenWC_LICENSE_TRUSTED_PROXIESconstant is defined - Without trusted proxy configuration, rate limiting uses
REMOTE_ADDRonly (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.6and pushed tomainbranch
2026-01-24 - Version 0.3.7 - Dashboard Improvements & Download Counter
Overview:
Fixed dashboard widget bugs, improved UI consistency, and added download tracking functionality with a new statistics widget.
Bug Fixes:
- Fixed: Dashboard widget "View All Licenses" link used wrong page slug (
wc-licensed-product-licensesinstead ofwc-licenses) - Fixed: Download links in customer account resulted in 404 errors due to missing query var registration
- Added
license-downloadendpoint registration during plugin activation inInstaller::activate() - Added
addDownloadQueryVar()method toDownloadControllerfor proper WordPress endpoint recognition
UI Improvements:
- Removed redundant "Status Breakdown" section from license statistics widget (info already shown in stat cards above)
- Changed License Types section to use card-style layout matching the stats row above
- Cleaned up unused CSS for status badges
New Features:
- Download counter for licensed product versions (tracked per version in database)
- New Download Statistics admin dashboard widget showing:
- Total downloads count
- Top 5 products by downloads
- Top 5 versions by downloads
New files:
src/Admin/DownloadWidgetController.php- Dashboard widget for download statistics
New methods in VersionManager:
incrementDownloadCount()- Atomically increment download count for a versiongetTotalDownloadCount()- Get total downloads across all versionsgetDownloadStatistics()- Get download stats grouped by product and version
Modified files:
src/Installer.php- Addeddownload_countcolumn to versions table, addedlicense-downloadendpoint registrationsrc/Product/ProductVersion.php- AddeddownloadCountproperty andgetDownloadCount()methodsrc/Product/VersionManager.php- Added download counting methodssrc/Frontend/DownloadController.php- Added query var registration, increment download count on file servesrc/Admin/DashboardWidgetController.php- Fixed URL, removed Status Breakdown, changed License Types to cardssrc/Plugin.php- Added DownloadWidgetController instantiation
Technical notes:
- Download count is incremented atomically using SQL
download_count = download_count + 1 - Statistics queries use SQL aggregation with product name enrichment via
wc_get_product() - WordPress endpoints require both
add_rewrite_endpoint()ANDquery_varsfilter registration - Existing installations need to flush rewrite rules (Settings > Permalinks > Save) or reactivate plugin
Release v0.3.7:
- Created release package:
releases/wc-licensed-product-0.3.7.zip(827 KB) - SHA256:
e93b2ab06f6d43c2179167090e07eda5db6809df6e391baece4ceba321cf33f6 - Tagged as
v0.3.7and pushed tomainbranch
2026-01-24 - Version 0.3.8 - Translation Bug Fix
Overview:
Fixed a critical translation bug that caused the settings page to crash with an ArgumentCountError.
Bug Fix:
- Fixed: Duplicate German translation string in
wc-licensed-product-de_CH.pocausingArgumentCountErrorin settings page - Root cause: The notification settings description was duplicated in the translation, resulting in two
%splaceholders when only one argument was passed tosprintf() - Location: wc-licensed-product-de_CH.po:322-328
Modified files:
languages/wc-licensed-product-de_CH.po- Removed duplicated translation stringlanguages/wc-licensed-product-de_CH.mo- Recompiled binary translation
Technical notes:
- Error was logged to
tmp/fatal-errors-2026-01-24.log - The German
msgstrcontained the same text twice, each with a%splaceholder sprintf()atSettingsController.php:221only provided one argument for the single%sin the English source- Translation strings with
%splaceholders must have exactly matching placeholder counts between source and translation
Dependency Updates:
- Updated
magdev/wc-licensed-product-clientfrom9f513a8to64d215c
Release v0.3.8:
- Created release package:
releases/wc-licensed-product-0.3.8.zip(829 KB) - SHA256:
50ad6966c5ab8db2257572084d2d8a820448df62615678e1576696f2c0cb383d - Tagged as
v0.3.8and pushed tomainbranch