You've already forked wc-licensed-product
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57e1b838cc | |||
| cfd34c9329 | |||
| fb4be7124b | |||
| 73ba7fb929 | |||
| 548b2ae8af | |||
| e0001c3f4e | |||
| a879be989c | |||
| 40c08bf474 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
releases/*
|
||||
|
||||
# Marketing texts (not part of plugin distribution)
|
||||
MARKETING.md
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.5] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- **Grafana Dashboard**: Example dashboard for license metrics monitoring
|
||||
- 24 panels organized into 4 sections: License Overview, Downloads & Versions, API Metrics, Errors & Rate Limiting
|
||||
- Template variables for data source and instance filtering
|
||||
- Includes example Prometheus alerting rules
|
||||
- **WP Prometheus Dashboard Integration**: Dashboard automatically registered with wp-prometheus
|
||||
- Appears in Settings > WP Prometheus > Dashboards when metrics are enabled
|
||||
- Uses `wp_prometheus_register_dashboards` hook for seamless integration
|
||||
- Documentation for Grafana dashboard installation and PromQL query examples
|
||||
|
||||
### New Files
|
||||
|
||||
- `docs/grafana-dashboard.json` - Complete Grafana dashboard with 24 panels
|
||||
- `docs/grafana-dashboard.md` - Installation and usage documentation
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated README with "Monitoring with Prometheus & Grafana" section
|
||||
|
||||
## [0.7.4] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- **Prometheus Metrics Integration**: Expose license and API metrics for monitoring
|
||||
- New "Metrics" settings tab with enable/disable toggle
|
||||
- License gauges: total by status, lifetime, expiring, expiring soon
|
||||
- Download gauges: total downloads, active versions count
|
||||
- API counters: requests by endpoint/result, rate limit exceeded events, validation errors by type
|
||||
- Requires [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) plugin
|
||||
|
||||
### New Files
|
||||
|
||||
- `src/Metrics/PrometheusController.php` - Prometheus metrics collection and registration
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
|
||||
- API counters stored persistently in WordPress options (`wclp_prometheus_counters`)
|
||||
- Static methods for incrementing counters from API controllers
|
||||
- Metrics only collected when enabled in settings
|
||||
|
||||
## [0.7.3] - 2026-02-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Docker Environment Support:** API Verification Secret now visible on customer licenses page in Docker environments
|
||||
- Added `ResponseSigner::getServerSecret()` method to check multiple sources for server secret
|
||||
- Checks PHP constant, `getenv()`, `$_ENV`, and `$_SERVER` in priority order
|
||||
- Maintains full backward compatibility with standard WordPress installations
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `Plugin.php` to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Root cause: Docker WordPress setups using `wp-config-docker.php` with `getenv_docker()` don't always define PHP constants
|
||||
- The environment variable was accessible but the constant wasn't being created
|
||||
- New `getServerSecret()` method centralizes all server secret retrieval logic
|
||||
|
||||
## [0.7.2] - 2026-01-29
|
||||
|
||||
### Added
|
||||
|
||||
124
CLAUDE.md
124
CLAUDE.md
@@ -32,7 +32,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
**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.
|
||||
|
||||
No pending roadmap items.
|
||||
### Known Bugs
|
||||
|
||||
None currently tracked.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -1945,3 +1947,123 @@ composer install
|
||||
- Automatically created by Gitea Actions CI/CD pipeline
|
||||
- Release package: 881 KiB with SHA256 checksum
|
||||
- First automated release - all future releases will use this workflow
|
||||
|
||||
**Additional fixes (same session):**
|
||||
|
||||
- Updated README.md with Auto-Updates section and Development section
|
||||
- Fixed CI/CD workflow to handle existing releases (delete before recreate)
|
||||
- When updating a tag, the workflow now checks for existing releases and deletes them first
|
||||
|
||||
**Lessons learned:**
|
||||
|
||||
- Gitea releases persist even when their tag is deleted - must delete release via API
|
||||
- Composer `symlink: false` doesn't always work - CI must manually replace symlinks with `cp -r`
|
||||
- Never create zip archives locally on this machine (fills up RAM indefinitely)
|
||||
- Gitea API endpoint for releases by tag: `GET /api/v1/repos/{owner}/{repo}/releases/tags/{tag}`
|
||||
|
||||
### 2026-02-01 - Bug Fix: API Verification Secret Not Visible
|
||||
|
||||
**Overview:**
|
||||
|
||||
Fixed the "API Verification Secret" (customer secret) not appearing on the customer account licenses page in Docker environments.
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
The `WC_LICENSE_SERVER_SECRET` constant was not being defined even though the environment variable was set. In Docker WordPress setups using `wp-config-docker.php`, the `getenv_docker()` function retrieves values from environment variables, but the constant wasn't being created properly. The plugin was only checking for the PHP constant, not the environment variable directly.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Added `ResponseSigner::getServerSecret()` static method that checks multiple sources for the server secret:
|
||||
|
||||
1. `WC_LICENSE_SERVER_SECRET` constant (standard WordPress configuration)
|
||||
2. `getenv('WC_LICENSE_SERVER_SECRET')` (Docker environments)
|
||||
3. `$_ENV['WC_LICENSE_SERVER_SECRET']` (some PHP configurations)
|
||||
4. `$_SERVER['WC_LICENSE_SERVER_SECRET']` (fallback)
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Api/ResponseSigner.php` - Added `getServerSecret()` method, updated `isSigningEnabled()` and `getCustomerSecretForLicense()` to use it
|
||||
- `src/Plugin.php` - Updated to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- The fix maintains backward compatibility with standard WordPress installations using constants
|
||||
- Docker environments can now use environment variables directly without needing the constant to be defined
|
||||
- All three methods (`isSigningEnabled()`, `getCustomerSecretForLicense()`, and constructor) now use the centralized `getServerSecret()` method
|
||||
|
||||
### 2026-02-03 - Version 0.7.4 - Prometheus Metrics Integration
|
||||
|
||||
**Overview:**
|
||||
|
||||
Added Prometheus metrics integration to expose license and API metrics for monitoring. Requires the WP Prometheus plugin.
|
||||
|
||||
**New files:**
|
||||
|
||||
- `src/Metrics/PrometheusController.php` - Prometheus metrics collection controller
|
||||
|
||||
**Implemented:**
|
||||
|
||||
- New "Metrics" settings section with enable/disable toggle
|
||||
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
|
||||
- License gauges using existing `LicenseManager::getStatistics()`:
|
||||
- `wclp_licenses_total{status}` - License counts by status
|
||||
- `wclp_licenses_lifetime_total` - Lifetime licenses count
|
||||
- `wclp_licenses_expiring_total` - Expiring licenses count
|
||||
- `wclp_licenses_expiring_soon` - Licenses expiring within 30 days
|
||||
- Download gauges using existing `VersionManager::getDownloadStatistics()`:
|
||||
- `wclp_downloads_total` - Total downloads
|
||||
- `wclp_versions_active_total` - Active product versions
|
||||
- API counters (stored in WordPress options for persistence):
|
||||
- `wclp_api_requests_total{endpoint,result}` - API requests by endpoint and result
|
||||
- `wclp_rate_limit_exceeded_total{endpoint}` - Rate limit exceeded events
|
||||
- `wclp_validation_errors_total{error_type}` - Validation errors by type
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Admin/SettingsController.php` - Added 'metrics' section with settings
|
||||
- `src/Api/RestApiController.php` - Added metric tracking for API requests
|
||||
- `src/Api/UpdateController.php` - Added metric tracking for update-check requests
|
||||
- `src/Plugin.php` - Initialize PrometheusController
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Metrics are only collected when enabled via settings toggle
|
||||
- Static methods allow increment from API controllers without dependency injection
|
||||
- Counter values persist across requests via `wclp_prometheus_counters` option
|
||||
- Gauges query database on each metric collection (uses existing statistics methods)
|
||||
|
||||
### 2026-02-03 - Grafana Dashboard & WP Prometheus Integration
|
||||
|
||||
**Overview:**
|
||||
|
||||
Added example Grafana dashboard JSON and integrated with wp-prometheus dashboard registration system.
|
||||
|
||||
**New files:**
|
||||
|
||||
- `docs/grafana-dashboard.json` - Complete Grafana dashboard with 24 panels
|
||||
- `docs/grafana-dashboard.md` - Installation and usage documentation
|
||||
|
||||
**Dashboard panels:**
|
||||
|
||||
- License Overview: Total, Active, Lifetime, Expiring Soon, Expired, Revoked stats + pie chart + time series
|
||||
- Downloads & Versions: Total downloads, active versions, download trends
|
||||
- API Metrics: Request rates by endpoint/result, pie chart breakdown, top requests table
|
||||
- Errors & Rate Limiting: Rate limit events, validation errors by type over time
|
||||
|
||||
**WP Prometheus integration:**
|
||||
|
||||
- Dashboard automatically registered via `wp_prometheus_register_dashboards` hook
|
||||
- Appears in Settings > WP Prometheus > Dashboards tab when metrics are enabled
|
||||
- Uses file-based registration with metadata (title, description, icon, plugin attribution)
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Metrics/PrometheusController.php` - Added `registerDashboard()` method and hook registration
|
||||
- `docs/grafana-dashboard.md` - Added wp-prometheus installation option as recommended method
|
||||
- `README.md` - Added "Monitoring with Prometheus & Grafana" section linking to dashboard docs
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Dashboard registration only occurs when metrics are enabled (same condition as metric collection)
|
||||
- Uses `dashicons-admin-network` icon for dashboard list
|
||||
- File path uses `WC_LICENSED_PRODUCT_PLUGIN_DIR` constant for reliable path resolution
|
||||
|
||||
32
README.md
32
README.md
@@ -393,6 +393,38 @@ The plugin sends automatic email notifications (configurable via WooCommerce > S
|
||||
- **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration
|
||||
- **License Expired**: Notification when a license auto-expires
|
||||
|
||||
## Monitoring with Prometheus & Grafana
|
||||
|
||||
The plugin integrates with [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose metrics for monitoring and alerting.
|
||||
|
||||
### Enable Metrics
|
||||
|
||||
1. Install and configure the wp-prometheus plugin
|
||||
2. Go to WooCommerce > Settings > Licensed Products > Metrics
|
||||
3. Enable "Prometheus Metrics"
|
||||
|
||||
### Available Metrics
|
||||
|
||||
**Gauges:**
|
||||
|
||||
- `wclp_licenses_total{status}` - License counts by status
|
||||
- `wclp_licenses_lifetime_total` - Lifetime licenses
|
||||
- `wclp_licenses_expiring_soon` - Licenses expiring within 30 days
|
||||
- `wclp_downloads_total` - Total file downloads
|
||||
- `wclp_versions_active_total` - Active product versions
|
||||
|
||||
**Counters:**
|
||||
|
||||
- `wclp_api_requests_total{endpoint,result}` - API requests by endpoint and result
|
||||
- `wclp_rate_limit_exceeded_total{endpoint}` - Rate limit events
|
||||
- `wclp_validation_errors_total{error_type}` - Validation errors by type
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
An example Grafana dashboard is included at [docs/grafana-dashboard.json](docs/grafana-dashboard.json).
|
||||
|
||||
See [docs/grafana-dashboard.md](docs/grafana-dashboard.md) for installation instructions, panel descriptions, and alerting examples.
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
|
||||
|
||||
1748
docs/grafana-dashboard.json
Normal file
1748
docs/grafana-dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
219
docs/grafana-dashboard.md
Normal file
219
docs/grafana-dashboard.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# Grafana Dashboard for WC Licensed Product
|
||||
|
||||
This dashboard provides comprehensive monitoring for the WC Licensed Product plugin using Prometheus metrics exposed via the [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) plugin.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **WP Prometheus Plugin** - Install and configure [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) on your WordPress site
|
||||
2. **Prometheus** - Configure Prometheus to scrape your WordPress metrics endpoint
|
||||
3. **Grafana** - Grafana 9.0+ with Prometheus data source configured
|
||||
4. **Enable Metrics** - In WordPress admin: WooCommerce > Settings > Licensed Products > Metrics > Enable Prometheus Metrics
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Via WP Prometheus Settings (Recommended)
|
||||
|
||||
When metrics are enabled, the dashboard is automatically registered with wp-prometheus:
|
||||
|
||||
1. Go to **Settings > WP Prometheus** in WordPress admin
|
||||
2. Navigate to the **Dashboards** tab
|
||||
3. Find "WC Licensed Product - License Metrics" in the list
|
||||
4. Click **Download JSON** to get the dashboard file
|
||||
5. Import the downloaded file into Grafana
|
||||
|
||||
### Option 2: Manual Import
|
||||
|
||||
1. Open Grafana and navigate to **Dashboards > Import**
|
||||
2. Upload the `grafana-dashboard.json` file or paste its contents
|
||||
3. Select your Prometheus data source
|
||||
4. Click **Import**
|
||||
|
||||
### Configure Variables
|
||||
|
||||
The dashboard includes two template variables:
|
||||
|
||||
- **datasource** - Select your Prometheus data source
|
||||
- **instance** - Filter by WordPress instance (useful for multi-site monitoring)
|
||||
|
||||
## Dashboard Panels
|
||||
|
||||
### License Overview
|
||||
|
||||
| Panel | Description |
|
||||
| --- | --- |
|
||||
| Total Licenses | Total count of all licenses |
|
||||
| Active Licenses | Licenses with `status=active` |
|
||||
| Lifetime Licenses | Licenses without expiration date |
|
||||
| Expiring Soon (30d) | Licenses expiring within 30 days |
|
||||
| Expired Licenses | Licenses with `status=expired` |
|
||||
| Revoked Licenses | Licenses with `status=revoked` |
|
||||
| Licenses by Status | Pie chart showing distribution |
|
||||
| License Status Over Time | Time series of license counts |
|
||||
|
||||
### Downloads & Versions
|
||||
|
||||
| Panel | Description |
|
||||
| --- | --- |
|
||||
| Total Downloads | Cumulative download count |
|
||||
| Active Product Versions | Number of active versions |
|
||||
| Downloads Over Time | Download trend graph |
|
||||
| Downloads (Selected Range) | Downloads in selected time range |
|
||||
|
||||
### API Metrics
|
||||
|
||||
| Panel | Description |
|
||||
| --- | --- |
|
||||
| API Requests (5m intervals) | Stacked bar chart by endpoint/result |
|
||||
| Requests by Endpoint | Donut chart of endpoint distribution |
|
||||
| Top API Requests | Table of most frequent requests |
|
||||
|
||||
### Errors & Rate Limiting
|
||||
|
||||
| Panel | Description |
|
||||
| --- | --- |
|
||||
| Rate Limit Events (Total) | Total HTTP 429 responses |
|
||||
| Validation Errors (Total) | Total validation failures |
|
||||
| Validation Errors Over Time | Error trend by type |
|
||||
| Validation Errors by Type | Pie chart breakdown |
|
||||
| Rate Limit Events by Endpoint | Rate limits per endpoint |
|
||||
|
||||
## Metrics Reference
|
||||
|
||||
### Gauges (current values)
|
||||
|
||||
```promql
|
||||
# Licenses by status
|
||||
wclp_licenses_total{status="active|expired|revoked|inactive"}
|
||||
|
||||
# Lifetime licenses (no expiration)
|
||||
wclp_licenses_lifetime_total
|
||||
|
||||
# Licenses with expiration date
|
||||
wclp_licenses_expiring_total
|
||||
|
||||
# Licenses expiring within 30 days
|
||||
wclp_licenses_expiring_soon
|
||||
|
||||
# Total downloads
|
||||
wclp_downloads_total
|
||||
|
||||
# Active product versions
|
||||
wclp_versions_active_total
|
||||
```
|
||||
|
||||
### Counters (cumulative)
|
||||
|
||||
```promql
|
||||
# API requests by endpoint and result
|
||||
wclp_api_requests_total{endpoint="validate|status|activate|update-check", result="success|error"}
|
||||
|
||||
# Rate limit exceeded events
|
||||
wclp_rate_limit_exceeded_total{endpoint="validate|status|activate|update-check"}
|
||||
|
||||
# Validation errors by type
|
||||
wclp_validation_errors_total{error_type="license_not_found|domain_mismatch|license_expired|license_revoked|..."}
|
||||
```
|
||||
|
||||
## Example Prometheus Queries
|
||||
|
||||
### Success Rate
|
||||
|
||||
```promql
|
||||
sum(rate(wclp_api_requests_total{result="success"}[5m])) /
|
||||
sum(rate(wclp_api_requests_total[5m])) * 100
|
||||
```
|
||||
|
||||
### Error Rate by Endpoint
|
||||
|
||||
```promql
|
||||
sum by (endpoint) (rate(wclp_api_requests_total{result="error"}[5m]))
|
||||
```
|
||||
|
||||
### License Churn (new activations)
|
||||
|
||||
```promql
|
||||
increase(wclp_licenses_total{status="active"}[1d])
|
||||
```
|
||||
|
||||
### Top Validation Errors
|
||||
|
||||
```promql
|
||||
topk(5, sum by (error_type) (wclp_validation_errors_total))
|
||||
```
|
||||
|
||||
## Alerting Examples
|
||||
|
||||
Add these alerts to your Prometheus alerting rules:
|
||||
|
||||
```yaml
|
||||
groups:
|
||||
- name: wc-licensed-product
|
||||
rules:
|
||||
# High rate limit events
|
||||
- alert: HighRateLimitEvents
|
||||
expr: increase(wclp_rate_limit_exceeded_total[5m]) > 10
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "High rate limiting on {{ $labels.endpoint }}"
|
||||
|
||||
# Many expiring licenses
|
||||
- alert: LicensesExpiringSoon
|
||||
expr: wclp_licenses_expiring_soon > 20
|
||||
for: 1h
|
||||
labels:
|
||||
severity: info
|
||||
annotations:
|
||||
summary: "{{ $value }} licenses expiring within 30 days"
|
||||
|
||||
# API error rate
|
||||
- alert: HighAPIErrorRate
|
||||
expr: |
|
||||
sum(rate(wclp_api_requests_total{result="error"}[5m])) /
|
||||
sum(rate(wclp_api_requests_total[5m])) > 0.1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "API error rate above 10%"
|
||||
```
|
||||
|
||||
## Prometheus Configuration
|
||||
|
||||
Add to your `prometheus.yml`:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'wordpress'
|
||||
metrics_path: '/metrics'
|
||||
scheme: https
|
||||
bearer_token: 'YOUR_WP_PROMETHEUS_TOKEN'
|
||||
static_configs:
|
||||
- targets: ['your-wordpress-site.com']
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No data showing
|
||||
|
||||
1. Verify wp-prometheus is installed and configured
|
||||
2. Check that metrics are enabled in WC Licensed Product settings
|
||||
3. Confirm Prometheus can reach your WordPress metrics endpoint
|
||||
4. Check the data source selection in Grafana
|
||||
|
||||
### Missing metrics
|
||||
|
||||
Some metrics only appear after relevant actions occur:
|
||||
|
||||
- `wclp_api_requests_total` - After API requests
|
||||
- `wclp_rate_limit_exceeded_total` - After rate limit events
|
||||
- `wclp_validation_errors_total` - After validation errors
|
||||
|
||||
### Counter resets
|
||||
|
||||
Counters persist in WordPress options and survive restarts. To reset:
|
||||
|
||||
```php
|
||||
\Jeremias\WcLicensedProduct\Metrics\PrometheusController::resetCounters();
|
||||
```
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@ final class SettingsController
|
||||
'auto-updates' => __('Auto-Updates', 'wc-licensed-product'),
|
||||
'defaults' => __('Default Settings', 'wc-licensed-product'),
|
||||
'notifications' => __('Notifications', 'wc-licensed-product'),
|
||||
'metrics' => __('Metrics', 'wc-licensed-product'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ final class SettingsController
|
||||
'auto-updates' => $this->getAutoUpdatesSettings(),
|
||||
'defaults' => $this->getDefaultsSettings(),
|
||||
'notifications' => $this->getNotificationsSettings(),
|
||||
'metrics' => $this->getMetricsSettings(),
|
||||
default => $this->getPluginLicenseSettings(),
|
||||
};
|
||||
}
|
||||
@@ -314,6 +316,32 @@ final class SettingsController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics settings
|
||||
*/
|
||||
private function getMetricsSettings(): array
|
||||
{
|
||||
return [
|
||||
'metrics_section_title' => [
|
||||
'name' => __('Prometheus Metrics', 'wc-licensed-product'),
|
||||
'type' => 'title',
|
||||
'desc' => __('Expose license and API metrics for Prometheus monitoring. Requires the WP Prometheus plugin to be installed and active.', 'wc-licensed-product'),
|
||||
'id' => 'wc_licensed_product_section_metrics',
|
||||
],
|
||||
'metrics_enabled' => [
|
||||
'name' => __('Enable Prometheus Metrics', 'wc-licensed-product'),
|
||||
'type' => 'checkbox',
|
||||
'desc' => __('Expose license statistics, API usage, and download metrics via Prometheus.', 'wc-licensed-product'),
|
||||
'id' => 'wc_licensed_product_metrics_enabled',
|
||||
'default' => 'no',
|
||||
],
|
||||
'metrics_section_end' => [
|
||||
'type' => 'sectionend',
|
||||
'id' => 'wc_licensed_product_section_metrics_end',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render settings tab content
|
||||
*/
|
||||
@@ -575,4 +603,12 @@ final class SettingsController
|
||||
wp_send_json_error(['message' => $error]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Prometheus metrics are enabled
|
||||
*/
|
||||
public static function isMetricsEnabled(): bool
|
||||
{
|
||||
return get_option('wc_licensed_product_metrics_enabled', 'no') === 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ final class ResponseSigner
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET')
|
||||
? WC_LICENSE_SERVER_SECRET
|
||||
: '';
|
||||
$this->serverSecret = self::getServerSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,7 +183,7 @@ final class ResponseSigner
|
||||
*/
|
||||
public static function getCustomerSecretForLicense(string $licenseKey): ?string
|
||||
{
|
||||
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';
|
||||
$serverSecret = self::getServerSecret();
|
||||
|
||||
if (empty($serverSecret)) {
|
||||
return null;
|
||||
@@ -201,6 +199,40 @@ final class ResponseSigner
|
||||
*/
|
||||
public static function isSigningEnabled(): bool
|
||||
{
|
||||
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
|
||||
return !empty(self::getServerSecret());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server secret from constant or environment variable
|
||||
*
|
||||
* Checks in order:
|
||||
* 1. WC_LICENSE_SERVER_SECRET constant (preferred)
|
||||
* 2. WC_LICENSE_SERVER_SECRET environment variable (Docker fallback)
|
||||
*
|
||||
* @return string The server secret, or empty string if not configured
|
||||
*/
|
||||
public static function getServerSecret(): string
|
||||
{
|
||||
// First check the constant (standard WordPress configuration)
|
||||
if (defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET)) {
|
||||
return WC_LICENSE_SERVER_SECRET;
|
||||
}
|
||||
|
||||
// Fallback to environment variable (Docker environments)
|
||||
$envSecret = getenv('WC_LICENSE_SERVER_SECRET');
|
||||
if ($envSecret !== false && !empty($envSecret)) {
|
||||
return $envSecret;
|
||||
}
|
||||
|
||||
// Also check $_ENV and $_SERVER (some PHP configurations)
|
||||
if (!empty($_ENV['WC_LICENSE_SERVER_SECRET'])) {
|
||||
return $_ENV['WC_LICENSE_SERVER_SECRET'];
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['WC_LICENSE_SERVER_SECRET'])) {
|
||||
return $_SERVER['WC_LICENSE_SERVER_SECRET'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Jeremias\WcLicensedProduct\Api;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
@@ -108,6 +109,10 @@ final class RestApiController
|
||||
'retry_after' => $retryAfter,
|
||||
], 429);
|
||||
$response->header('Retry-After', (string) $retryAfter);
|
||||
|
||||
// Track rate limit event for metrics
|
||||
PrometheusController::incrementRateLimitExceeded('api');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -209,6 +214,16 @@ final class RestApiController
|
||||
|
||||
$statusCode = $this->getStatusCodeForResult($result);
|
||||
|
||||
// Track metrics
|
||||
if ($result['valid']) {
|
||||
PrometheusController::incrementApiRequest('validate', 'success');
|
||||
} else {
|
||||
PrometheusController::incrementApiRequest('validate', 'error');
|
||||
if (!empty($result['error'])) {
|
||||
PrometheusController::incrementValidationError($result['error']);
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response($result, $statusCode);
|
||||
}
|
||||
|
||||
@@ -247,6 +262,9 @@ final class RestApiController
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
PrometheusController::incrementApiRequest('status', 'error');
|
||||
PrometheusController::incrementValidationError('license_not_found');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'valid' => false,
|
||||
'error' => 'license_not_found',
|
||||
@@ -254,6 +272,8 @@ final class RestApiController
|
||||
], 404);
|
||||
}
|
||||
|
||||
PrometheusController::incrementApiRequest('status', 'success');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'valid' => $license->isValid(),
|
||||
'status' => $license->getStatus(),
|
||||
@@ -280,6 +300,9 @@ final class RestApiController
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
|
||||
if (!$license) {
|
||||
PrometheusController::incrementApiRequest('activate', 'error');
|
||||
PrometheusController::incrementValidationError('license_not_found');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'license_not_found',
|
||||
@@ -288,6 +311,9 @@ final class RestApiController
|
||||
}
|
||||
|
||||
if (!$license->isValid()) {
|
||||
PrometheusController::incrementApiRequest('activate', 'error');
|
||||
PrometheusController::incrementValidationError('license_invalid');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'license_invalid',
|
||||
@@ -299,6 +325,8 @@ final class RestApiController
|
||||
|
||||
// Check if already activated on this domain
|
||||
if ($license->getDomain() === $normalizedDomain) {
|
||||
PrometheusController::incrementApiRequest('activate', 'success');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
|
||||
@@ -307,6 +335,9 @@ final class RestApiController
|
||||
|
||||
// Check if can activate on another domain
|
||||
if (!$license->canActivate()) {
|
||||
PrometheusController::incrementApiRequest('activate', 'error');
|
||||
PrometheusController::incrementValidationError('max_activations_reached');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'max_activations_reached',
|
||||
@@ -318,6 +349,9 @@ final class RestApiController
|
||||
$success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);
|
||||
|
||||
if (!$success) {
|
||||
PrometheusController::incrementApiRequest('activate', 'error');
|
||||
PrometheusController::incrementValidationError('activation_failed');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'error' => 'activation_failed',
|
||||
@@ -325,6 +359,8 @@ final class RestApiController
|
||||
], 500);
|
||||
}
|
||||
|
||||
PrometheusController::incrementApiRequest('activate', 'success');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('License activated successfully.', 'wc-licensed-product'),
|
||||
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Jeremias\WcLicensedProduct\Api;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
|
||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||
use Jeremias\WcLicensedProduct\Product\ProductVersion;
|
||||
use WP_REST_Request;
|
||||
@@ -113,6 +114,10 @@ final class UpdateController
|
||||
'retry_after' => $retryAfter,
|
||||
], 429);
|
||||
$response->header('Retry-After', (string) $retryAfter);
|
||||
|
||||
// Track rate limit event for metrics
|
||||
PrometheusController::incrementRateLimitExceeded('update-check');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@@ -179,10 +184,14 @@ final class UpdateController
|
||||
$validationResult = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||
|
||||
if (!$validationResult['valid']) {
|
||||
$errorType = $validationResult['error'] ?? 'license_invalid';
|
||||
PrometheusController::incrementApiRequest('update-check', 'error');
|
||||
PrometheusController::incrementValidationError($errorType);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'update_available' => false,
|
||||
'error' => $validationResult['error'] ?? 'license_invalid',
|
||||
'error' => $errorType,
|
||||
'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'),
|
||||
], $validationResult['error'] === 'license_not_found' ? 404 : 403);
|
||||
}
|
||||
@@ -190,6 +199,9 @@ final class UpdateController
|
||||
// Get license to access product ID
|
||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||
if (!$license) {
|
||||
PrometheusController::incrementApiRequest('update-check', 'error');
|
||||
PrometheusController::incrementValidationError('license_not_found');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'update_available' => false,
|
||||
@@ -202,6 +214,9 @@ final class UpdateController
|
||||
$product = wc_get_product($productId);
|
||||
|
||||
if (!$product) {
|
||||
PrometheusController::incrementApiRequest('update-check', 'error');
|
||||
PrometheusController::incrementValidationError('product_not_found');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => false,
|
||||
'update_available' => false,
|
||||
@@ -214,6 +229,8 @@ final class UpdateController
|
||||
$latestVersion = $this->getLatestVersionForLicense($license);
|
||||
|
||||
if (!$latestVersion) {
|
||||
PrometheusController::incrementApiRequest('update-check', 'success');
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'update_available' => false,
|
||||
@@ -230,6 +247,8 @@ final class UpdateController
|
||||
// Build response
|
||||
$response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable);
|
||||
|
||||
PrometheusController::incrementApiRequest('update-check', 'success');
|
||||
|
||||
return new WP_REST_Response($response);
|
||||
}
|
||||
|
||||
|
||||
282
src/Metrics/PrometheusController.php
Normal file
282
src/Metrics/PrometheusController.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* Prometheus Metrics Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Metrics
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Metrics;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||
|
||||
/**
|
||||
* Exposes license and API metrics for Prometheus monitoring
|
||||
*/
|
||||
final class PrometheusController
|
||||
{
|
||||
/**
|
||||
* Option name for storing API counters
|
||||
*/
|
||||
private const COUNTERS_OPTION = 'wclp_prometheus_counters';
|
||||
|
||||
private LicenseManager $licenseManager;
|
||||
private VersionManager $versionManager;
|
||||
|
||||
public function __construct(LicenseManager $licenseManager, VersionManager $versionManager)
|
||||
{
|
||||
$this->licenseManager = $licenseManager;
|
||||
$this->versionManager = $versionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks for Prometheus metrics collection
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Only register if metrics are enabled
|
||||
if (!SettingsController::isMetricsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action('wp_prometheus_collect_metrics', [$this, 'collectMetrics']);
|
||||
add_action('wp_prometheus_register_dashboards', [$this, 'registerDashboard']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Grafana dashboard with wp-prometheus
|
||||
*
|
||||
* @param object $provider The dashboard provider object
|
||||
*/
|
||||
public function registerDashboard(object $provider): void
|
||||
{
|
||||
$dashboardFile = WC_LICENSED_PRODUCT_PLUGIN_DIR . 'docs/grafana-dashboard.json';
|
||||
|
||||
if (!file_exists($dashboardFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider->register_dashboard('wc-licensed-product', [
|
||||
'title' => __('WC Licensed Product - License Metrics', 'wc-licensed-product'),
|
||||
'description' => __('Monitor license status, downloads, API usage, and validation errors.', 'wc-licensed-product'),
|
||||
'icon' => 'dashicons-admin-network',
|
||||
'file' => $dashboardFile,
|
||||
'plugin' => 'WC Licensed Product',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect and register all metrics
|
||||
*
|
||||
* @param object $collector The Prometheus collector object
|
||||
*/
|
||||
public function collectMetrics(object $collector): void
|
||||
{
|
||||
$this->collectLicenseMetrics($collector);
|
||||
$this->collectDownloadMetrics($collector);
|
||||
$this->collectApiMetrics($collector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect license-related metrics
|
||||
*/
|
||||
private function collectLicenseMetrics(object $collector): void
|
||||
{
|
||||
$stats = $this->licenseManager->getStatistics();
|
||||
|
||||
// License count by status (gauge)
|
||||
$licensesByStatus = $collector->register_gauge(
|
||||
'wclp_licenses_total',
|
||||
'Total number of licenses by status',
|
||||
['status']
|
||||
);
|
||||
|
||||
foreach ($stats['by_status'] as $status => $count) {
|
||||
$licensesByStatus->set($count, [$status]);
|
||||
}
|
||||
|
||||
// Lifetime licenses (gauge)
|
||||
$lifetimeLicenses = $collector->register_gauge(
|
||||
'wclp_licenses_lifetime_total',
|
||||
'Total number of lifetime licenses'
|
||||
);
|
||||
$lifetimeLicenses->set($stats['lifetime']);
|
||||
|
||||
// Expiring licenses (gauge)
|
||||
$expiringLicenses = $collector->register_gauge(
|
||||
'wclp_licenses_expiring_total',
|
||||
'Total number of licenses with expiration date'
|
||||
);
|
||||
$expiringLicenses->set($stats['expiring']);
|
||||
|
||||
// Licenses expiring soon - next 30 days (gauge)
|
||||
$expiringSoon = $collector->register_gauge(
|
||||
'wclp_licenses_expiring_soon',
|
||||
'Licenses expiring within 30 days'
|
||||
);
|
||||
$expiringSoon->set($stats['expiring_soon']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect download-related metrics
|
||||
*/
|
||||
private function collectDownloadMetrics(object $collector): void
|
||||
{
|
||||
$stats = $this->versionManager->getDownloadStatistics();
|
||||
|
||||
// Total downloads (gauge)
|
||||
$totalDownloads = $collector->register_gauge(
|
||||
'wclp_downloads_total',
|
||||
'Total number of file downloads'
|
||||
);
|
||||
$totalDownloads->set($stats['total']);
|
||||
|
||||
// Active versions count (gauge)
|
||||
$activeVersions = $collector->register_gauge(
|
||||
'wclp_versions_active_total',
|
||||
'Total number of active product versions'
|
||||
);
|
||||
$activeVersions->set($this->countActiveVersions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect API-related metrics (counters)
|
||||
*/
|
||||
private function collectApiMetrics(object $collector): void
|
||||
{
|
||||
$counters = $this->getCounters();
|
||||
|
||||
// API requests by endpoint and result (counter)
|
||||
$apiRequests = $collector->register_counter(
|
||||
'wclp_api_requests_total',
|
||||
'Total API requests by endpoint and result',
|
||||
['endpoint', 'result']
|
||||
);
|
||||
|
||||
foreach ($counters['api_requests'] ?? [] as $key => $count) {
|
||||
[$endpoint, $result] = explode(':', $key);
|
||||
$apiRequests->incBy($count, [$endpoint, $result]);
|
||||
}
|
||||
|
||||
// Rate limit exceeded events (counter)
|
||||
$rateLimitExceeded = $collector->register_counter(
|
||||
'wclp_rate_limit_exceeded_total',
|
||||
'Total rate limit exceeded events by endpoint',
|
||||
['endpoint']
|
||||
);
|
||||
|
||||
foreach ($counters['rate_limit'] ?? [] as $endpoint => $count) {
|
||||
$rateLimitExceeded->incBy($count, [$endpoint]);
|
||||
}
|
||||
|
||||
// Validation errors by type (counter)
|
||||
$validationErrors = $collector->register_counter(
|
||||
'wclp_validation_errors_total',
|
||||
'Total validation errors by error type',
|
||||
['error_type']
|
||||
);
|
||||
|
||||
foreach ($counters['validation_errors'] ?? [] as $errorType => $count) {
|
||||
$validationErrors->incBy($count, [$errorType]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count active product versions
|
||||
*/
|
||||
private function countActiveVersions(): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = \Jeremias\WcLicensedProduct\Installer::getVersionsTable();
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$tableName} WHERE is_active = 1"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored counters
|
||||
*/
|
||||
private function getCounters(): array
|
||||
{
|
||||
$counters = get_option(self::COUNTERS_OPTION, []);
|
||||
return is_array($counters) ? $counters : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment an API request counter
|
||||
*
|
||||
* @param string $endpoint The API endpoint (validate, status, activate, update-check)
|
||||
* @param string $result The result (success or error)
|
||||
*/
|
||||
public static function incrementApiRequest(string $endpoint, string $result): void
|
||||
{
|
||||
if (!SettingsController::isMetricsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$counters = get_option(self::COUNTERS_OPTION, []);
|
||||
if (!is_array($counters)) {
|
||||
$counters = [];
|
||||
}
|
||||
|
||||
$key = "{$endpoint}:{$result}";
|
||||
$counters['api_requests'][$key] = ($counters['api_requests'][$key] ?? 0) + 1;
|
||||
|
||||
update_option(self::COUNTERS_OPTION, $counters, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment rate limit exceeded counter
|
||||
*
|
||||
* @param string $endpoint The API endpoint
|
||||
*/
|
||||
public static function incrementRateLimitExceeded(string $endpoint): void
|
||||
{
|
||||
if (!SettingsController::isMetricsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$counters = get_option(self::COUNTERS_OPTION, []);
|
||||
if (!is_array($counters)) {
|
||||
$counters = [];
|
||||
}
|
||||
|
||||
$counters['rate_limit'][$endpoint] = ($counters['rate_limit'][$endpoint] ?? 0) + 1;
|
||||
|
||||
update_option(self::COUNTERS_OPTION, $counters, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment validation error counter
|
||||
*
|
||||
* @param string $errorType The error type (license_not_found, domain_mismatch, etc.)
|
||||
*/
|
||||
public static function incrementValidationError(string $errorType): void
|
||||
{
|
||||
if (!SettingsController::isMetricsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$counters = get_option(self::COUNTERS_OPTION, []);
|
||||
if (!is_array($counters)) {
|
||||
$counters = [];
|
||||
}
|
||||
|
||||
$counters['validation_errors'][$errorType] = ($counters['validation_errors'][$errorType] ?? 0) + 1;
|
||||
|
||||
update_option(self::COUNTERS_OPTION, $counters, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all counters (useful for testing or maintenance)
|
||||
*/
|
||||
public static function resetCounters(): void
|
||||
{
|
||||
delete_option(self::COUNTERS_OPTION);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ use Jeremias\WcLicensedProduct\Frontend\AccountController;
|
||||
use Jeremias\WcLicensedProduct\Frontend\DownloadController;
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||
use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker;
|
||||
@@ -147,7 +148,7 @@ final class Plugin
|
||||
new LicenseEmailController($this->licenseManager);
|
||||
|
||||
// Initialize response signing if server secret is configured
|
||||
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') {
|
||||
if (ResponseSigner::isSigningEnabled()) {
|
||||
(new ResponseSigner())->register();
|
||||
}
|
||||
|
||||
@@ -171,6 +172,9 @@ final class Plugin
|
||||
if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) {
|
||||
PluginUpdateChecker::getInstance()->register();
|
||||
}
|
||||
|
||||
// Initialize Prometheus metrics if enabled
|
||||
(new PrometheusController($this->licenseManager, $this->versionManager))->register();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WooCommerce Licensed Product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||
* Version: 0.7.2
|
||||
* Version: 0.7.5
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.7.2');
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.7.5');
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user