feat: Implement OAuth license activation flow
- Add LicenseConnect.tsx focused OAuth confirmation page in customer SPA - Add /licenses/oauth/validate and /licenses/oauth/confirm API endpoints - Update App.tsx to render license-connect outside BaseLayout (no header/footer) - Add license_activation_method field to product settings in Admin SPA - Create LICENSING_MODULE.md with comprehensive OAuth flow documentation - Update API_ROUTES.md with license module endpoints
This commit is contained in:
228
.agent/reports/email-notification-audit-2026-01-29.md
Normal file
228
.agent/reports/email-notification-audit-2026-01-29.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Email Notification System Audit
|
||||
|
||||
**Date:** January 29, 2026
|
||||
**Status:** ✅ System Architecture Sound, Minor Issues Identified
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The WooNooW email notification system is **well-architected** with proper async handling, template rendering, and event management. The main components work together correctly. However, some potential gaps and improvements were identified.
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[WooCommerce Hooks] --> B[EmailManager]
|
||||
B --> C{Is WooNooW Mode?}
|
||||
C -->|Yes| D[EmailRenderer]
|
||||
C -->|No| E[WC Default Emails]
|
||||
D --> F[TemplateProvider]
|
||||
F --> G[Get Template]
|
||||
G --> H[Replace Variables]
|
||||
H --> I[Parse Markdown/Cards]
|
||||
I --> J[wp_mail]
|
||||
J --> K[WooEmailOverride Intercepts]
|
||||
K --> L[MailQueue::enqueue]
|
||||
L --> M[Action Scheduler]
|
||||
M --> N[MailQueue::sendNow]
|
||||
N --> O[Actual wp_mail]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [EmailManager.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EmailManager.php) | Hooks WC order events, disables WC emails, routes to renderer |
|
||||
| [EmailRenderer.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EmailRenderer.php) | Renders templates, replaces variables, parses markdown |
|
||||
| [TemplateProvider.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/TemplateProvider.php) | Manages templates, defaults, variable definitions |
|
||||
| [EventRegistry.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EventRegistry.php) | Central registry of all notification events |
|
||||
| [NotificationManager.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/NotificationManager.php) | Validates settings, dispatches to channels |
|
||||
| [WooEmailOverride.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Mail/WooEmailOverride.php) | Intercepts wp_mail via `pre_wp_mail` filter |
|
||||
| [MailQueue.php](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Mail/MailQueue.php) | Async queue via Action Scheduler |
|
||||
|
||||
---
|
||||
|
||||
## Email Flow Trace
|
||||
|
||||
### 1. Event Trigger
|
||||
- WooCommerce fires hooks like `woocommerce_order_status_pending_to_processing`
|
||||
- `EmailManager::init_hooks()` registers callbacks for these hooks
|
||||
|
||||
### 2. EmailManager Processing
|
||||
```php
|
||||
// In EmailManager.php
|
||||
add_action('woocommerce_order_status_pending_to_processing', [$this, 'send_order_processing_email']);
|
||||
```
|
||||
- Checks if WooNooW mode enabled: `is_enabled()`
|
||||
- Checks if event enabled: `is_event_enabled()`
|
||||
- Calls `send_email($event_id, $recipient_type, $order)`
|
||||
|
||||
### 3. Email Rendering
|
||||
- `EmailRenderer::render()` called
|
||||
- Gets template from `TemplateProvider::get_template()`
|
||||
- Gets variables from `get_variables()` (order, customer, product data)
|
||||
- Replaces `{variable}` placeholders
|
||||
- Parses `[card]` markdown syntax
|
||||
- Wraps in HTML template from `templates/emails/base.html`
|
||||
|
||||
### 4. wp_mail Interception
|
||||
- `wp_mail()` is called with rendered HTML
|
||||
- `WooEmailOverride::interceptMail()` catches via `pre_wp_mail` filter
|
||||
- Returns `true` to short-circuit synchronous send
|
||||
|
||||
### 5. Queue & Async Send
|
||||
- `MailQueue::enqueue()` stores payload in `wp_options` (temp)
|
||||
- Schedules `woonoow/mail/send` action via Action Scheduler
|
||||
- `MailQueue::sendNow()` runs asynchronously:
|
||||
- Retrieves payload from options
|
||||
- Disables `WooEmailOverride` to prevent loop
|
||||
- Calls actual `wp_mail()`
|
||||
- Deletes temp option
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### ✅ Working Correctly
|
||||
|
||||
1. **Async Email Queue**: Properly prevents timeout issues
|
||||
2. **Template System**: Variables replaced correctly
|
||||
3. **Event Registry**: Single source of truth
|
||||
4. **Subscription Events**: Registered via `woonoow_notification_events_registry` filter
|
||||
5. **Global Toggle**: WooNooW vs WooCommerce mode works
|
||||
6. **WC Email Disable**: Default emails properly disabled when WooNooW active
|
||||
|
||||
### ⚠️ Potential Issues
|
||||
|
||||
#### 1. Missing Subscription Variable Population in EmailRenderer
|
||||
**Location:** [EmailRenderer.php:147-299](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EmailRenderer.php#L147-L299)
|
||||
|
||||
**Issue:** `get_variables()` handles `WC_Order`, `WC_Product`, `WC_Customer` but NOT subscription objects. Subscription notifications pass data like:
|
||||
```php
|
||||
$data = [
|
||||
'subscription' => $subscription, // Custom subscription object
|
||||
'customer' => $user,
|
||||
'product' => $product,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
**Impact:** Subscription email variables like `{subscription_id}`, `{billing_period}`, `{next_payment_date}` may not be replaced.
|
||||
|
||||
**Recommendation:** Add subscription variable population in `EmailRenderer::get_variables()`.
|
||||
|
||||
---
|
||||
|
||||
#### 2. EmailRenderer Type Check for Subscription
|
||||
**Location:** [EmailRenderer.php:121-137](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EmailRenderer.php#L121-L137)
|
||||
|
||||
**Issue:** `get_recipient_email()` only checks for `WC_Order` and `WC_Customer`. For subscriptions, `$data` is an array, so recipient email extraction fails.
|
||||
|
||||
**Impact:** Subscription emails may not find recipient email.
|
||||
|
||||
**Recommendation:** Handle array data or subscription object in `get_recipient_email()`.
|
||||
|
||||
---
|
||||
|
||||
#### 3. SubscriptionModule Sends to NotificationManager, Not EmailManager
|
||||
**Location:** [SubscriptionModule.php:529-531](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Modules/Subscription/SubscriptionModule.php#L529-L531)
|
||||
|
||||
**Code:**
|
||||
```php
|
||||
\WooNooW\Core\Notifications\NotificationManager::send($event_id, 'email', $data);
|
||||
```
|
||||
|
||||
**Issue:** This goes through `NotificationManager`, which calls its own `send_email()` that uses `EmailRenderer::render()`. The `EmailRenderer::render()` method receives `$data['subscription']` but doesn't know how to handle it.
|
||||
|
||||
**Impact:** Subscription email rendering may fail silently.
|
||||
|
||||
---
|
||||
|
||||
#### 4. No Error Logging in Email Rendering Failures
|
||||
**Location:** [EmailRenderer.php:48-57](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Core/Notifications/EmailRenderer.php#L48-L57)
|
||||
|
||||
**Issue:** When `get_template_settings()` returns null or `get_recipient_email()` returns null, the function returns null silently with only an empty debug log statement.
|
||||
|
||||
**Recommendation:** Add proper `error_log()` calls for debugging.
|
||||
|
||||
---
|
||||
|
||||
#### 5. Duplicate wp_mail Calls
|
||||
**Location:** Multiple places call `wp_mail()` directly:
|
||||
- `EmailManager::send_email()` (line 521)
|
||||
- `EmailManager::send_password_reset_email()` (line 406)
|
||||
- `NotificationManager::send_email()` (line 170)
|
||||
- `NotificationsController` test endpoint (line 1013)
|
||||
- `CampaignManager` (lines 275, 329)
|
||||
- `NewsletterController` (line 203)
|
||||
|
||||
**Issue:** All these are intercepted by `WooEmailOverride`, which is correct. However, if `WooEmailOverride` is disabled (testing mode), all send synchronously.
|
||||
|
||||
**Status:** Working as designed.
|
||||
|
||||
---
|
||||
|
||||
## Subscription Email Gap Analysis
|
||||
|
||||
The subscription module has these events defined but needs variable population:
|
||||
|
||||
| Event | Variables Needed |
|
||||
|-------|-----------------|
|
||||
| `subscription_pending_cancellation` | subscription_id, product_name, end_date |
|
||||
| `subscription_cancelled` | subscription_id, cancel_reason |
|
||||
| `subscription_expired` | subscription_id, product_name |
|
||||
| `subscription_paused` | subscription_id, product_name |
|
||||
| `subscription_resumed` | subscription_id, product_name |
|
||||
| `subscription_renewal_failed` | subscription_id, failed_count, payment_link |
|
||||
| `subscription_renewal_payment_due` | subscription_id, payment_link |
|
||||
| `subscription_renewal_reminder` | subscription_id, next_payment_date |
|
||||
|
||||
**Required Fix:** Add subscription data handling to `EmailRenderer::get_variables()`.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### High Priority
|
||||
|
||||
1. **Fix `EmailRenderer::get_variables()`** - Add handling for subscription data arrays
|
||||
2. **Fix `EmailRenderer::get_recipient_email()`** - Handle array data with customer key
|
||||
|
||||
### Medium Priority
|
||||
|
||||
3. **Add error logging** - Replace empty debug conditions with actual logging
|
||||
4. **Clean up debug conditions** - Many `if (defined('WP_DEBUG') && WP_DEBUG) {}` are empty
|
||||
|
||||
### Low Priority
|
||||
|
||||
5. **Consolidate email sending paths** - Consider routing all through one method
|
||||
6. **Add email send failure tracking** - Log failed sends for troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Test Scripts Available
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `check-settings.php` | Diagnose notification settings |
|
||||
| `test-email-flow.php` | Interactive email testing dashboard |
|
||||
| `test-email-direct.php` | Direct wp_mail testing |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
Comprehensive docs exist:
|
||||
- [NOTIFICATION_SYSTEM.md](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/NOTIFICATION_SYSTEM.md)
|
||||
- [EMAIL_DEBUGGING_GUIDE.md](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/EMAIL_DEBUGGING_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The email notification system is **production-ready** for order-related notifications. The main gap is **subscription email variable population**, which requires updates to `EmailRenderer.php` to properly handle subscription data and extract variables.
|
||||
391
.agent/reports/license-activation-research-2026-01-31.md
Normal file
391
.agent/reports/license-activation-research-2026-01-31.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# OAuth-Style License Activation Research Report
|
||||
|
||||
**Date:** January 31, 2026
|
||||
**Objective:** Design a strict license activation system requiring vendor site authentication
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
After researching Elementor Pro, Tutor LMS, EDD Software Licensing, and industry standards, the **redirect-based OAuth-like activation flow** is the most secure and user-friendly approach. This pattern:
|
||||
- Prevents license key sharing by tying activation to user accounts
|
||||
- Provides better UX than manual key entry
|
||||
- Enables flexible license management
|
||||
- Creates an anti-piracy layer beyond just key validation
|
||||
|
||||
---
|
||||
|
||||
## Industry Analysis
|
||||
|
||||
### 1. Elementor Pro
|
||||
|
||||
| Aspect | Implementation |
|
||||
|--------|----------------|
|
||||
| **Flow** | "Connect & Activate" button → redirect to Elementor.com → login required → authorize connection → return to WP Admin |
|
||||
| **Why Listed** | Market leader with 5M+ users; sets the standard for premium plugin activation |
|
||||
| **Anti-Piracy** | Account-tied activation; no ability to share just a license key |
|
||||
| **Fallback** | Manual key entry via hidden URL parameter `?mode=manually` |
|
||||
|
||||
**Key Pattern:** Elementor never shows the license key in the normal flow—users authenticate with their account, not a key.
|
||||
|
||||
---
|
||||
|
||||
### 2. Tutor LMS (Themeum)
|
||||
|
||||
| Aspect | Implementation |
|
||||
|--------|----------------|
|
||||
| **Flow** | License settings → Enter key → "Connect" button → redirect to Themeum → login → confirm connection |
|
||||
| **Why Listed** | Popular LMS plugin; hybrid approach (key + account verification) |
|
||||
| **Anti-Piracy** | License keys tied to specific domains registered in user account |
|
||||
| **License Display** | Keys visible in account dashboard for copy-paste |
|
||||
|
||||
**Key Pattern:** Requires domain registration in vendor account before activation works.
|
||||
|
||||
---
|
||||
|
||||
### 3. Easy Digital Downloads (EDD) Software Licensing
|
||||
|
||||
| Aspect | Implementation |
|
||||
|--------|----------------|
|
||||
| **Flow** | API-based: plugin sends key + site URL to vendor → server validates → returns activation status |
|
||||
| **Why Listed** | Powers many WordPress plugin vendors (WPForms, MonsterInsights, etc.) |
|
||||
| **Anti-Piracy** | Activation limits (e.g., 1 site, 5 sites, unlimited); site URL tracking |
|
||||
| **Management** | Customer can manage activations in their EDD account |
|
||||
|
||||
**Key Pattern:** Traditional key-based but with strict activation limits and site tracking.
|
||||
|
||||
---
|
||||
|
||||
### 4. WooCommerce Software License Manager
|
||||
|
||||
| Aspect | Implementation |
|
||||
|--------|----------------|
|
||||
| **Flow** | REST API with key + secret authentication |
|
||||
| **Why Listed** | Common for WooCommerce-based vendors |
|
||||
| **Anti-Piracy** | API-key authentication; activation records |
|
||||
|
||||
**Key Pattern:** Programmatic API access, less user-facing UX focus.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Identified
|
||||
|
||||
### Anti-Piracy Measures
|
||||
|
||||
| Measure | Effectiveness | UX Impact |
|
||||
|---------|---------------|-----------|
|
||||
| **Account authentication required** | ★★★★★ | Minor inconvenience |
|
||||
| **Activation limits per license** | ★★★★☆ | None |
|
||||
| **Domain/URL binding** | ★★★★☆ | None |
|
||||
| **Tying updates/support to valid license** | ★★★★★ | Incentivizes purchase |
|
||||
| **Periodic license re-validation** | ★★★☆☆ | Can cause issues |
|
||||
| **Encrypted API communication (HTTPS)** | ★★★★★ | None |
|
||||
|
||||
### UX Considerations
|
||||
|
||||
| Consideration | Priority |
|
||||
|---------------|----------|
|
||||
| One-click activation (minimal friction) | High |
|
||||
| Clear error messages | High |
|
||||
| License status visibility in WP Admin | Medium |
|
||||
| Easy deactivation for site migrations | High |
|
||||
| Fallback manual activation | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Security Comparison
|
||||
|
||||
| Method | Piracy Resistance | Implementation Complexity |
|
||||
|--------|-------------------|---------------------------|
|
||||
| **Simple key validation** | Low | Simple |
|
||||
| **Key + site URL binding** | Medium | Medium |
|
||||
| **Key + activation limits** | Medium-High | Medium |
|
||||
| **OAuth redirect + account tie** | High | Complex |
|
||||
| **OAuth + key + activation limits** | Very High | Complex |
|
||||
|
||||
---
|
||||
|
||||
## Your Proposed Flow Analysis
|
||||
|
||||
### Original Flow Points
|
||||
|
||||
1. User navigates to license page → clicks [ACTIVATE]
|
||||
2. Redirect to vendor site (licensing.woonoow.com or similar)
|
||||
3. Vendor site: login required
|
||||
4. Vendor shows licenses for user's account, filtered by product
|
||||
5. User selects license to connect
|
||||
6. Click "Connect This Site"
|
||||
7. Return to `return_url` after short delay
|
||||
|
||||
### Identified Gaps
|
||||
|
||||
| Gap | Risk | Solution |
|
||||
|-----|------|----------|
|
||||
| No state parameter | CSRF attack possible | Add signed `state` token |
|
||||
| No nonce verification | Replay attacks | Include one-time nonce |
|
||||
| Return URL manipulation | Redirect hijacking | Validate return URL on server |
|
||||
| No deactivation flow | User can't migrate | Add disconnect button |
|
||||
|
||||
---
|
||||
|
||||
## Perfected Implementation Plan
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ WP ADMIN (Client Site) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Settings → License │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Status: Not Connected │ │
|
||||
│ │ [🔗 Connect & Activate] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ Redirect with signed params
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ VENDOR SITE (License Server) │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ /license/connect? │
|
||||
│ product_id=woonoow-pro& │
|
||||
│ site_url=https://customer-site.com& │
|
||||
│ return_url=https://customer-site.com/wp-admin/...& │
|
||||
│ state=<signed_token>& │
|
||||
│ nonce=<one_time_code> │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ 1. Force login if not authenticated │
|
||||
│ 2. Show licenses owned by user for this product │
|
||||
│ 3. User selects: "Pro License (3/5 sites used)" │
|
||||
│ 4. Click [Connect This Site] │
|
||||
│ 5. Server records activation │
|
||||
│ 6. Redirect back with activation token │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ Callback with activation token
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ WP ADMIN (Client Site) │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ Callback handler: │
|
||||
│ 1. Verify state matches stored value │
|
||||
│ 2. Exchange activation_token for license_key via API │
|
||||
│ 3. Store license_key securely │
|
||||
│ 4. Show success: "License activated successfully!" │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Status: ✅ Active │ │
|
||||
│ │ License: Pro (expires Dec 31, 2026) │ │
|
||||
│ │ Sites: 4/5 activated │ │
|
||||
│ │ [Disconnect] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Detailed Flow
|
||||
|
||||
#### Phase 1: Initiation (Client Plugin)
|
||||
|
||||
```php
|
||||
// User clicks "Connect & Activate"
|
||||
$params = [
|
||||
'product_id' => 'woonoow-pro',
|
||||
'site_url' => home_url(),
|
||||
'return_url' => admin_url('admin.php?page=woonoow-license&action=callback'),
|
||||
'nonce' => wp_create_nonce('woonoow_license_connect'),
|
||||
'state' => $this->generate_state_token(), // Signed, stored in transient
|
||||
'timestamp' => time(),
|
||||
];
|
||||
|
||||
$redirect_url = 'https://licensing.woonoow.com/connect?' . http_build_query($params);
|
||||
wp_redirect($redirect_url);
|
||||
```
|
||||
|
||||
#### Phase 2: Authentication (Vendor Server)
|
||||
|
||||
1. **Login Gate**: If user not logged in → redirect to login with `?redirect=/connect?...`
|
||||
2. **Validate Request**: Check `state`, `nonce`, `timestamp` (reject if >10 min old)
|
||||
3. **Fetch User Licenses**: Query licenses owned by authenticated user for `product_id`
|
||||
4. **Display License Selector**:
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Connect site-name.com to your license │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ ○ WooNooW Pro - Agency (Unlimited sites) │
|
||||
│ ● WooNooW Pro - Business (3/5 sites) ←selected │
|
||||
│ ○ WooNooW Pro - Personal (1/1 sites) [FULL] │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ [Cancel] [Connect This Site] │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
5. **Record Activation**: Insert into `license_activations` table
|
||||
6. **Generate Callback**: Redirect to `return_url` with:
|
||||
- `activation_token`: Short-lived token (5 min expiry)
|
||||
- `state`: Original state for verification
|
||||
|
||||
#### Phase 3: Callback (Client Plugin)
|
||||
|
||||
```php
|
||||
// Handle callback
|
||||
$activation_token = sanitize_text_field($_GET['activation_token']);
|
||||
$state = sanitize_text_field($_GET['state']);
|
||||
|
||||
// 1. Verify state matches stored transient
|
||||
if (!$this->verify_state_token($state)) {
|
||||
wp_die('Invalid state. Possible CSRF attack.');
|
||||
}
|
||||
|
||||
// 2. Exchange token for license details via secure API
|
||||
$response = wp_remote_post('https://licensing.woonoow.com/api/v1/token/exchange', [
|
||||
'body' => [
|
||||
'activation_token' => $activation_token,
|
||||
'site_url' => home_url(),
|
||||
],
|
||||
]);
|
||||
|
||||
// 3. Store license data
|
||||
$license_data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
update_option('woonoow_license', [
|
||||
'key' => $license_data['license_key'],
|
||||
'status' => 'active',
|
||||
'expires' => $license_data['expires_at'],
|
||||
'tier' => $license_data['tier'],
|
||||
'sites_used' => $license_data['sites_used'],
|
||||
'sites_max' => $license_data['sites_max'],
|
||||
]);
|
||||
|
||||
// 4. Redirect with success
|
||||
wp_redirect(admin_url('admin.php?page=woonoow-license&activated=1'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Security Parameters
|
||||
|
||||
| Parameter | Purpose | Implementation |
|
||||
|-----------|---------|----------------|
|
||||
| `state` | CSRF protection | HMAC-signed, stored in transient, expires 10 min |
|
||||
| `nonce` | Replay prevention | One-time use, verified on server |
|
||||
| `timestamp` | Request freshness | Reject requests >10 min old |
|
||||
| `activation_token` | Secure exchange | Short-lived (5 min), single-use |
|
||||
| `site_url` | Domain binding | Stored with activation record |
|
||||
|
||||
---
|
||||
|
||||
### Database Schema (Vendor Server)
|
||||
|
||||
```sql
|
||||
CREATE TABLE license_activations (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
license_id BIGINT NOT NULL,
|
||||
site_url VARCHAR(255) NOT NULL,
|
||||
activation_token VARCHAR(64),
|
||||
token_expires_at DATETIME,
|
||||
activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_check DATETIME,
|
||||
status ENUM('active', 'deactivated') DEFAULT 'active',
|
||||
metadata JSON,
|
||||
UNIQUE KEY unique_license_site (license_id, site_url),
|
||||
FOREIGN KEY (license_id) REFERENCES licenses(id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Deactivation Flow
|
||||
|
||||
```
|
||||
Client: [Disconnect] button clicked
|
||||
→ POST /api/v1/license/deactivate
|
||||
→ Body: { license_key, site_url }
|
||||
→ Server removes activation record
|
||||
→ Client clears stored license
|
||||
→ Show "Disconnected" status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Periodic Validation
|
||||
|
||||
```php
|
||||
// Cron check every 24 hours
|
||||
add_action('woonoow_daily_license_check', function() {
|
||||
$license = get_option('woonoow_license');
|
||||
if (!$license) return;
|
||||
|
||||
$response = wp_remote_post('https://licensing.woonoow.com/api/v1/license/validate', [
|
||||
'body' => [
|
||||
'license_key' => $license['key'],
|
||||
'site_url' => home_url(),
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if ($data['status'] !== 'active') {
|
||||
update_option('woonoow_license', ['status' => 'invalid']);
|
||||
// Optionally disable premium features
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (Vendor Server)
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/connect` | GET | OAuth-like authorization page |
|
||||
| `/api/v1/token/exchange` | POST | Exchange activation token for license |
|
||||
| `/api/v1/license/validate` | POST | Validate license status |
|
||||
| `/api/v1/license/deactivate` | POST | Remove site activation |
|
||||
| `/api/v1/license/info` | GET | Get license details |
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Your Flow vs. Perfected
|
||||
|
||||
| Aspect | Your Original | Perfected |
|
||||
|--------|---------------|-----------|
|
||||
| CSRF Protection | ❌ None | ✅ State token |
|
||||
| Replay Prevention | ❌ None | ✅ Nonce + timestamp |
|
||||
| Token Exchange | ❌ Direct return | ✅ Secure exchange |
|
||||
| Return URL Security | ❌ Unvalidated | ✅ Server whitelist |
|
||||
| Deactivation | ❌ Not mentioned | ✅ Full flow |
|
||||
| Periodic Validation | ❌ Not mentioned | ✅ Daily cron |
|
||||
| Fallback | ❌ None | ✅ Manual key entry |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Server-Side (Licensing Portal)
|
||||
1. Create `/connect` authorization page
|
||||
2. Build license selection UI
|
||||
3. Implement activation recording
|
||||
4. Create token exchange API
|
||||
|
||||
### Phase 2: Client-Side (WooNooW Plugin)
|
||||
1. Create Settings → License admin page
|
||||
2. Implement connect redirect
|
||||
3. Handle callback and token exchange
|
||||
4. Store license securely
|
||||
5. Add disconnect functionality
|
||||
|
||||
### Phase 3: Validation & Updates
|
||||
1. Implement periodic license checks
|
||||
2. Gate premium features behind valid license
|
||||
3. Integrate with plugin update checker
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
| Source | Relevance |
|
||||
|--------|-----------|
|
||||
| Elementor Pro Activation | Primary reference for UX flow |
|
||||
| Tutor LMS / Themeum | Hybrid key+account approach |
|
||||
| OAuth 2.0 Authorization Code Flow | Security pattern basis |
|
||||
| EDD Software Licensing | Activation limits pattern |
|
||||
| OWASP API Security | State/nonce implementation |
|
||||
212
.agent/reports/product-flow-audit-2026-01-29.md
Normal file
212
.agent/reports/product-flow-audit-2026-01-29.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Product Create/Update Flow Audit Report
|
||||
|
||||
**Date:** 2026-01-29
|
||||
**Scope:** Full trace of product creation, update, SKU validation, variation handling, virtual product setting, and customer-facing add-to-cart
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Total Issues Found: 4**
|
||||
- **CRITICAL:** 2
|
||||
- **WARNING:** 1
|
||||
- **INFO:** 1
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 🔴 Issue #1: SKU Validation Blocks Variation Updates
|
||||
|
||||
**Severity:** CRITICAL
|
||||
**Location:** [ProductsController.php#L1009](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Api/ProductsController.php#L1009)
|
||||
|
||||
**Problem:**
|
||||
When updating a variable product, the `save_product_variations` method sets SKU unconditionally:
|
||||
```php
|
||||
if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']);
|
||||
```
|
||||
|
||||
WooCommerce validates that SKU must be unique across all products. When updating a variation that already has that SKU, WooCommerce throws an exception because it sees the SKU as a duplicate.
|
||||
|
||||
**Root Cause:**
|
||||
WooCommerce's `set_sku()` method checks for uniqueness but doesn't know the variation already owns that SKU during the update.
|
||||
|
||||
**Fix Required:**
|
||||
Before setting SKU, check if the new SKU is the same as the current SKU:
|
||||
```php
|
||||
if (isset($var_data['sku'])) {
|
||||
$current_sku = $variation->get_sku();
|
||||
$new_sku = $var_data['sku'];
|
||||
// Only set if different (to avoid WC duplicate check issue)
|
||||
if ($current_sku !== $new_sku) {
|
||||
$variation->set_sku($new_sku);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 Issue #2: Variation Selection Fails (Attribute Format Mismatch)
|
||||
|
||||
**Severity:** CRITICAL
|
||||
**Location:**
|
||||
- Backend: [ShopController.php#L363-365](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Frontend/ShopController.php#L363)
|
||||
- Frontend: [Product/index.tsx#L97-127](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/customer-spa/src/pages/Product/index.tsx#L97)
|
||||
|
||||
**Problem:**
|
||||
"Please select all product options" error appears even when a variation is selected.
|
||||
|
||||
**Root Cause:**
|
||||
Format mismatch between backend API and frontend matching logic:
|
||||
|
||||
| Source | Format | Example |
|
||||
|--------|--------|---------|
|
||||
| API `variations.attributes` | `attribute_pa_color: "red"` | Lowercase, prefixed |
|
||||
| API `attributes` | `name: "Color"` | Human-readable |
|
||||
| Frontend `selectedAttributes` | `Color: "Red"` | Human-readable, case preserved |
|
||||
|
||||
The matching logic at lines 100-120 has complex normalization but may fail at edge cases:
|
||||
- Taxonomy attributes use `pa_` prefix (e.g., `attribute_pa_color`)
|
||||
- Custom attributes use direct prefix (e.g., `attribute_size`)
|
||||
- The comparison normalizes both sides but attribute names in `selectedAttributes` are human-readable labels
|
||||
|
||||
**Fix Required:**
|
||||
Improve variation matching by normalizing attribute names consistently:
|
||||
|
||||
```typescript
|
||||
// In find matching variation logic:
|
||||
const variation = (product.variations as any[]).find(v => {
|
||||
return Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
||||
const normalizedAttrName = attrName.toLowerCase();
|
||||
const normalizedValue = attrValue.toLowerCase();
|
||||
|
||||
// Try all possible attribute key formats
|
||||
const possibleKeys = [
|
||||
`attribute_${normalizedAttrName}`,
|
||||
`attribute_pa_${normalizedAttrName}`,
|
||||
normalizedAttrName
|
||||
];
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
if (key in v.attributes) {
|
||||
return v.attributes[key].toLowerCase() === normalizedValue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Warning Issues
|
||||
|
||||
### 🟡 Issue #3: Virtual Product Setting May Not Persist for Variable Products
|
||||
|
||||
**Severity:** WARNING
|
||||
**Location:** [ProductsController.php#L496-498](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Api/ProductsController.php#L496)
|
||||
|
||||
**Problem:**
|
||||
User reports cannot change product to virtual. Investigation shows:
|
||||
- Admin-SPA correctly sends `virtual: true` in payload
|
||||
- Backend `update_product` correctly calls `$product->set_virtual()`
|
||||
- However, for variable products, virtual status may need to be set on each variation
|
||||
|
||||
**Observation:**
|
||||
The backend code at lines 496-498 handles virtual correctly:
|
||||
```php
|
||||
if (isset($data['virtual'])) {
|
||||
$product->set_virtual((bool) $data['virtual']);
|
||||
}
|
||||
```
|
||||
|
||||
**Potential Issue:**
|
||||
WooCommerce may ignore parent product's virtual flag for variable products. Each variation may need to be set as virtual individually.
|
||||
|
||||
**Fix Required:**
|
||||
When saving variations, also propagate virtual flag:
|
||||
```php
|
||||
// In save_product_variations, after setting other fields:
|
||||
if ($product->is_virtual()) {
|
||||
$variation->set_virtual(true);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Info Issues
|
||||
|
||||
### ℹ️ Issue #4: Missing Error Handling in Add-to-Cart Backend
|
||||
|
||||
**Severity:** INFO
|
||||
**Location:** [CartController.php#L202-203](file:///Users/dwindown/Local%20Sites/woonoow/app/public/wp-content/plugins/woonoow/includes/Api/Controllers/CartController.php#L202)
|
||||
|
||||
**Observation:**
|
||||
When `add_to_cart()` returns false, the error message is generic:
|
||||
```php
|
||||
if (!$cart_item_key) {
|
||||
return new WP_Error('add_to_cart_failed', 'Failed to add product to cart', ['status' => 400]);
|
||||
}
|
||||
```
|
||||
|
||||
WooCommerce may have more specific notices in `wc_notice` stack that could provide better error messages.
|
||||
|
||||
**Enhancement:**
|
||||
```php
|
||||
if (!$cart_item_key) {
|
||||
$notices = wc_get_notices('error');
|
||||
$message = !empty($notices) ? $notices[0]['notice'] : 'Failed to add product to cart';
|
||||
wc_clear_notices();
|
||||
return new WP_Error('add_to_cart_failed', $message, ['status' => 400]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Flow Summary
|
||||
|
||||
### Product Update Flow
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Admin SPA->>ProductsController: PUT /products/{id}
|
||||
ProductsController->>WC_Product: set_name, set_sku, etc.
|
||||
ProductsController->>WC_Product: set_virtual, set_downloadable
|
||||
ProductsController->>ProductsController: save_product_variations()
|
||||
ProductsController->>WC_Product_Variation: set_sku (BUG: no duplicate check)
|
||||
WC_Product_Variation-->>WooCommerce: validate_sku()
|
||||
WooCommerce-->>ProductsController: Exception (duplicate SKU)
|
||||
```
|
||||
|
||||
### Add-to-Cart Flow
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Customer SPA->>Product Page: Select variation
|
||||
Product Page->>useState: selectedAttributes = {Color: "Red"}
|
||||
Product Page->>useEffect: Find matching variation
|
||||
Note right of Product Page: Mismatch: API has attribute_pa_color
|
||||
Product Page-->>useState: selectedVariation = null
|
||||
Customer->>Product Page: Click Add to Cart
|
||||
Product Page->>Customer: "Please select all product options"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `ProductsController.php` | Fix SKU check in `save_product_variations` |
|
||||
| `Product/index.tsx` | Fix variation matching logic |
|
||||
| `ProductsController.php` | Propagate virtual to variations |
|
||||
| `CartController.php` | (Optional) Improve error messages |
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
After fixes:
|
||||
1. Create a variable product with SKU on variations
|
||||
2. Edit the product without changing SKU → should save successfully
|
||||
3. Add products to cart → verify variation selection works
|
||||
4. Test virtual product setting on simple and variable products
|
||||
Reference in New Issue
Block a user