You've already forked wc-licensed-product
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5826c744dc | |||
| 3a81544f30 | |||
| 89493aa5b6 | |||
| 46e5b5a1c5 | |||
| b89225c6d7 | |||
| 0ebd2d0103 | |||
| 6a10eada8c | |||
| f4da9e116a | |||
| 601a4f6da2 | |||
| 0758caefc7 | |||
| bcd3481ea3 | |||
| 60fb5cc13c | |||
| 1dc128a1e5 | |||
| f32758ab28 | |||
| ac1814cbb0 | |||
| 2d6bfa219a | |||
| 302f2e76ca | |||
| 5938aaed1b | |||
| 630a5859d3 |
228
.gitea/workflows/release.yml
Normal file
228
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,228 @@
|
||||
name: Create Release Package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip, intl, gettext
|
||||
tools: composer:v2
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate --no-check-lock --no-check-all
|
||||
|
||||
- name: Install Composer dependencies (production)
|
||||
run: |
|
||||
composer config platform.php 8.3.0
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
|
||||
- name: Fix vendor symlink
|
||||
run: |
|
||||
# If client is a symlink, replace with actual files
|
||||
if [ -L "vendor/magdev/wc-licensed-product-client" ]; then
|
||||
echo "Found symlink, replacing with actual files..."
|
||||
TARGET=$(readlink -f vendor/magdev/wc-licensed-product-client)
|
||||
rm vendor/magdev/wc-licensed-product-client
|
||||
cp -r "$TARGET" vendor/magdev/wc-licensed-product-client
|
||||
fi
|
||||
ls -la vendor/magdev/
|
||||
|
||||
- name: Install gettext
|
||||
run: apt-get update && apt-get install -y gettext
|
||||
|
||||
- name: Compile translations
|
||||
run: |
|
||||
for po in languages/*.po; do
|
||||
if [ -f "$po" ]; then
|
||||
mo="${po%.po}.mo"
|
||||
echo "Compiling $po to $mo"
|
||||
msgfmt -o "$mo" "$po"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify plugin version matches tag
|
||||
run: |
|
||||
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wc-licensed-product.php | head -1)
|
||||
TAG_VERSION=${{ steps.version.outputs.version }}
|
||||
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Version verified: $PLUGIN_VERSION"
|
||||
|
||||
- name: Create release directory
|
||||
run: mkdir -p releases
|
||||
|
||||
- name: Build release package
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
PLUGIN_NAME="wc-licensed-product"
|
||||
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
|
||||
|
||||
cd ..
|
||||
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
|
||||
-x "${PLUGIN_NAME}/.git/*" \
|
||||
-x "${PLUGIN_NAME}/.gitea/*" \
|
||||
-x "${PLUGIN_NAME}/.github/*" \
|
||||
-x "${PLUGIN_NAME}/.vscode/*" \
|
||||
-x "${PLUGIN_NAME}/.claude/*" \
|
||||
-x "${PLUGIN_NAME}/CLAUDE.md" \
|
||||
-x "${PLUGIN_NAME}/wp-core" \
|
||||
-x "${PLUGIN_NAME}/wp-core/*" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins" \
|
||||
-x "${PLUGIN_NAME}/wp-plugins/*" \
|
||||
-x "${PLUGIN_NAME}/releases/*" \
|
||||
-x "${PLUGIN_NAME}/composer.lock" \
|
||||
-x "${PLUGIN_NAME}/*.log" \
|
||||
-x "${PLUGIN_NAME}/.gitignore" \
|
||||
-x "${PLUGIN_NAME}/.gitmodules" \
|
||||
-x "${PLUGIN_NAME}/.editorconfig" \
|
||||
-x "${PLUGIN_NAME}/phpcs.xml*" \
|
||||
-x "${PLUGIN_NAME}/phpunit.xml*" \
|
||||
-x "${PLUGIN_NAME}/tests/*" \
|
||||
-x "${PLUGIN_NAME}/*.po~" \
|
||||
-x "${PLUGIN_NAME}/*.bak" \
|
||||
-x "${PLUGIN_NAME}/lib/*" \
|
||||
-x "${PLUGIN_NAME}/lib/*/.git/*" \
|
||||
-x "${PLUGIN_NAME}/vendor/magdev/*/.git/*" \
|
||||
-x "${PLUGIN_NAME}/vendor/magdev/*/CLAUDE.md" \
|
||||
-x "*.DS_Store"
|
||||
|
||||
cd "${PLUGIN_NAME}"
|
||||
echo "Created: ${RELEASE_FILE}"
|
||||
ls -lh "${RELEASE_FILE}"
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
PLUGIN_NAME="wc-licensed-product"
|
||||
|
||||
cd releases
|
||||
sha256sum "${PLUGIN_NAME}-${VERSION}.zip" > "${PLUGIN_NAME}-${VERSION}.zip.sha256"
|
||||
echo "SHA256:"
|
||||
cat "${PLUGIN_NAME}-${VERSION}.zip.sha256"
|
||||
|
||||
- name: Verify package structure
|
||||
run: |
|
||||
set +o pipefail
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
PLUGIN_NAME="wc-licensed-product"
|
||||
|
||||
echo "Package contents (first 50 entries):"
|
||||
unzip -l "releases/${PLUGIN_NAME}-${VERSION}.zip" | head -50 || true
|
||||
|
||||
# Verify main plugin file exists
|
||||
if unzip -l "releases/${PLUGIN_NAME}-${VERSION}.zip" | grep -q "${PLUGIN_NAME}/${PLUGIN_NAME}.php"; then
|
||||
echo "Main plugin file: OK"
|
||||
else
|
||||
echo "ERROR: Main plugin file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify vendor directory included
|
||||
if unzip -l "releases/${PLUGIN_NAME}-${VERSION}.zip" | grep -q "${PLUGIN_NAME}/vendor/"; then
|
||||
echo "Vendor directory: OK"
|
||||
else
|
||||
echo "ERROR: Vendor directory not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify lib directory excluded
|
||||
if unzip -l "releases/${PLUGIN_NAME}-${VERSION}.zip" | grep -q "${PLUGIN_NAME}/lib/"; then
|
||||
echo "WARNING: lib/ directory should be excluded"
|
||||
else
|
||||
echo "lib/ excluded: OK"
|
||||
fi
|
||||
|
||||
- name: Extract changelog for release notes
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
|
||||
if [ -z "$NOTES" ]; then
|
||||
NOTES="Release version ${VERSION}"
|
||||
fi
|
||||
echo "$NOTES" > release_notes.txt
|
||||
echo "Release notes extracted"
|
||||
|
||||
- name: Create Gitea Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
TAG_NAME=${{ github.ref_name }}
|
||||
PLUGIN_NAME="wc-licensed-product"
|
||||
|
||||
PRERELEASE="false"
|
||||
if [[ "$TAG_NAME" == *-* ]]; then
|
||||
PRERELEASE="true"
|
||||
fi
|
||||
|
||||
BODY=$(cat release_notes.txt)
|
||||
|
||||
# Check if release already exists and delete it
|
||||
EXISTING_RELEASE=$(curl -s \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
|
||||
|
||||
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||
echo "Deleting existing release ID: $EXISTING_ID"
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
|
||||
echo "Existing release deleted"
|
||||
fi
|
||||
|
||||
# Create release via Gitea API
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||
|
||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||
|
||||
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||
echo "Failed to create release:"
|
||||
echo "$RELEASE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release ID: $RELEASE_ID"
|
||||
|
||||
# Upload release assets
|
||||
for file in "releases/${PLUGIN_NAME}-${VERSION}.zip" "releases/${PLUGIN_NAME}-${VERSION}.zip.sha256"; do
|
||||
if [ -f "$file" ]; then
|
||||
FILENAME=$(basename "$file")
|
||||
echo "Uploading $FILENAME..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||
echo "Uploaded $FILENAME"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Release created: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "lib/wc-licensed-product-client"]
|
||||
path = lib/wc-licensed-product-client
|
||||
url = ../wc-licensed-product-client.git
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.2] - 2026-01-29
|
||||
|
||||
### Added
|
||||
|
||||
- **Gitea CI/CD Pipeline**: Automated release workflow triggered on version tags
|
||||
- Automatic package creation with proper WordPress subdirectory structure
|
||||
- SHA256 checksum generation for package integrity
|
||||
- Changelog extraction for release notes
|
||||
- Pre-release detection for hyphenated tags (e.g., `v0.7.2-rc1`)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Git Submodule Migration**: `magdev/wc-licensed-product-client` is now a git submodule
|
||||
- Located at `lib/wc-licensed-product-client` instead of being fetched via Composer VCS
|
||||
- Composer now uses `path` type repository pointing to local submodule
|
||||
- Improves version control clarity and development workflow
|
||||
- Symlinked to `vendor/` during `composer install`
|
||||
|
||||
### Developer Notes
|
||||
|
||||
- New file: `.gitea/workflows/release.yml` for CI/CD automation
|
||||
- Updated `composer.json`: Repository type changed from `vcs` to `path`
|
||||
- Created `.gitmodules` for submodule tracking
|
||||
- Release packages now exclude `lib/` directory (vendor has installed copy)
|
||||
- Submodule checkout required: `git submodule update --init --recursive`
|
||||
|
||||
## [0.7.1] - 2026-01-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CRITICAL:** Fixed API Verification Secret not displayed in PHP fallback template on customer account licenses page
|
||||
- Response signing now includes `/update-check` endpoint (was missing from signed routes)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `magdev/wc-licensed-product-client` dependency to v0.2.2
|
||||
- Updated `symfony/http-client` dependency to v7.4.5
|
||||
|
||||
### Technical Details
|
||||
|
||||
- Added customer secret display to `displayLicensesFallback()` method in `AccountController`
|
||||
- Added `/update-check` route to `ResponseSigner::shouldSign()` method for consistent signature headers
|
||||
- Verified server implementation aligns with updated client library documentation
|
||||
|
||||
## [0.7.0] - 2026-01-28
|
||||
|
||||
### Security
|
||||
|
||||
118
CLAUDE.md
118
CLAUDE.md
@@ -32,14 +32,7 @@ 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.
|
||||
|
||||
### Version 0.7.0
|
||||
|
||||
This is a security version. It includes a full security audit and a remote check of a live version of this plugin on <https://shop.magdev.cc>. The shop is the property of the plugin developer, all actions are permitted.
|
||||
|
||||
- Check the sourcecode for best practises of all involved components, including checks for SQLi, XSRF, XSS and similar techniques
|
||||
- Check the remote version for the OWASP Top 10
|
||||
- Check the whole licensing workflow
|
||||
- Minimize the thread vectors
|
||||
No pending roadmap items.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -60,6 +53,13 @@ This is a security version. It includes a full security audit and a remote check
|
||||
- Nonce verification on form submissions
|
||||
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
|
||||
- Direct file access prevention via `ABSPATH` check
|
||||
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
|
||||
- Rate limiting on API endpoints (configurable via `WC_LICENSE_RATE_LIMIT`)
|
||||
- Rate limiting on frontend operations (transfers: 5/hour, downloads: 30/hour)
|
||||
- CSV import limits (2MB max, 1000 rows max, 5-minute cooldown)
|
||||
- IP detection with proxy support via `IpDetectionTrait` (supports `WC_LICENSE_TRUSTED_PROXIES`)
|
||||
- SQL injection prevention using `$wpdb->prepare()` throughout
|
||||
- Secure download URLs with hash verification using `hash_equals()`
|
||||
|
||||
### Translation Ready
|
||||
|
||||
@@ -1843,3 +1843,105 @@ Security-focused release with comprehensive audit and hardening. Performed OWASP
|
||||
- Created release package: `releases/wc-licensed-product-0.7.0.zip` (883 KB)
|
||||
- SHA256: `12f8452316e350273003f36bf6d7b7121a7bedc9a6964c3d0732d26318d94c18`
|
||||
- Tagged as `v0.7.0` and pushed to `main` branch
|
||||
|
||||
### 2026-01-28 - Version 0.7.1 - Bug Fixes & Client Compatibility
|
||||
|
||||
**Overview:**
|
||||
|
||||
Bug fix release ensuring compatibility with updated `magdev/wc-licensed-product-client` v0.2.2 and fixing API Verification Secret display.
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
- **CRITICAL:** Fixed API Verification Secret not displaying on customer account licenses page when using PHP fallback (Twig unavailable)
|
||||
- Fixed `/update-check` endpoint responses not being signed (missing from `ResponseSigner::shouldSign()`)
|
||||
|
||||
**Dependency Updates:**
|
||||
|
||||
- Updated `magdev/wc-licensed-product-client` from `760e1e7` to `56abe8a` (v0.2.2)
|
||||
- Updated `symfony/http-client` from v7.4.4 to v7.4.5
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Frontend/AccountController.php` - Added customer secret display to PHP fallback method `displayLicensesFallback()`
|
||||
- `src/Api/ResponseSigner.php` - Added `/update-check` to `shouldSign()` method
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- PHP fallback template now includes the collapsible API Verification Secret section matching the Twig template
|
||||
- All four API endpoints (`/validate`, `/status`, `/activate`, `/update-check`) now include signature headers when `WC_LICENSE_SERVER_SECRET` is configured
|
||||
- Client library v0.2.2 verified compatible with server implementation
|
||||
|
||||
**Release v0.7.1:**
|
||||
|
||||
- Created release package: `releases/wc-licensed-product-0.7.1.zip` (886 KB)
|
||||
- SHA256: `6ffd0bdf47395436bbc28a029eff4c6d065f2b5b64c687b96ae36a74c3ee34ef`
|
||||
- Tagged as `v0.7.1` and pushed to `main` branch
|
||||
|
||||
### 2026-01-29 - Version 0.7.2 - Git Submodule & CI/CD Pipeline
|
||||
|
||||
**Overview:**
|
||||
|
||||
Infrastructure release converting the client library dependency to a git submodule and implementing automated CI/CD releases via Gitea Actions.
|
||||
|
||||
**Git Submodule Migration:**
|
||||
|
||||
- Converted `magdev/wc-licensed-product-client` from Composer VCS dependency to git submodule
|
||||
- Submodule located at `lib/wc-licensed-product-client`
|
||||
- Composer uses `path` type repository pointing to local submodule
|
||||
- Symlinked to `vendor/magdev/wc-licensed-product-client` during `composer install`
|
||||
|
||||
**Gitea CI/CD Pipeline:**
|
||||
|
||||
- New workflow at `.gitea/workflows/release.yml`
|
||||
- Triggers on version tags (`v*`)
|
||||
- Automated steps:
|
||||
- Checkout with recursive submodules
|
||||
- PHP 8.3 setup with required extensions
|
||||
- Composer dependency installation (production only)
|
||||
- Translation compilation (`.po` to `.mo`)
|
||||
- Version verification against plugin header
|
||||
- Release package creation with proper exclusions
|
||||
- SHA256 checksum generation
|
||||
- Package structure verification
|
||||
- Changelog extraction for release notes
|
||||
- Gitea release creation with asset upload
|
||||
- Pre-release detection for hyphenated tags
|
||||
|
||||
**New files:**
|
||||
|
||||
- `.gitea/workflows/release.yml` - Gitea Actions workflow for automated releases
|
||||
- `.gitmodules` - Git submodule configuration (created by git)
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `composer.json` - Changed repository type from `vcs` to `path`, URL to `lib/wc-licensed-product-client`
|
||||
- `CHANGELOG.md` - Added v0.7.2 release notes
|
||||
- `CLAUDE.md` - Removed v0.7.2 from roadmap, added session history
|
||||
|
||||
**Package Exclusions:**
|
||||
|
||||
Release packages exclude: `.git/`, `.gitea/`, `.gitmodules`, `lib/` (submodule source), `vendor/**/.git`, `tests/`, `CLAUDE.md`, `*.po~`, `wp-core`, `wp-plugins`, `composer.lock`
|
||||
|
||||
**Developer Workflow Changes:**
|
||||
|
||||
After cloning the repository, developers must now run:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
composer install
|
||||
```
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Path repository uses `*` version constraint with `symlink: false` option
|
||||
- CI replaces symlink with actual files via `cp -r` before packaging
|
||||
- CI uses `actions/checkout@v4` with `submodules: recursive` for proper submodule initialization
|
||||
- Release creation uses direct Gitea API calls (`/api/v1/repos/.../releases`)
|
||||
- Requires `SRC_GITEA_TOKEN` secret configured in Gitea repository settings
|
||||
- Workflow completed successfully: 57 seconds, all checks passed
|
||||
|
||||
**Release v0.7.2:**
|
||||
|
||||
- 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
|
||||
|
||||
101
README.md
101
README.md
@@ -21,9 +21,12 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
||||
- **Version Binding**: Optional binding to major software versions
|
||||
- **Expiration Support**: Set license validity periods or lifetime licenses
|
||||
- **Rate Limiting**: API endpoints protected with configurable rate limiting (default: 30 requests/minute)
|
||||
- **Frontend Rate Limiting**: Transfer requests (5/hour) and downloads (30/hour) protected against abuse
|
||||
- **Trusted Proxy Support**: Configurable trusted proxies for accurate rate limiting behind CDNs
|
||||
- **Checkout Blocks**: Full support for WooCommerce Checkout Blocks (default since WC 8.3+)
|
||||
- **Self-Licensing**: The plugin can validate its own license (for commercial distribution)
|
||||
- **WordPress Auto-Updates**: Receive plugin updates through WordPress's native update system
|
||||
- **Automated Releases**: CI/CD pipeline for consistent release packaging
|
||||
|
||||
### Customer Features
|
||||
|
||||
@@ -132,17 +135,26 @@ When a customer purchases a licensed product, they must enter the domain where t
|
||||
3. Upload a CSV file (supports exported format or simplified format)
|
||||
4. Choose options: skip header row, update existing licenses
|
||||
|
||||
**Import Limits (Security):**
|
||||
|
||||
- Maximum file size: 2MB
|
||||
- Maximum rows per import: 1000
|
||||
- Cooldown between imports: 5 minutes
|
||||
|
||||
## Security
|
||||
|
||||
The plugin implements several security best practices:
|
||||
|
||||
- **Input Sanitization**: All user inputs are sanitized using WordPress functions
|
||||
- **Output Escaping**: All output is escaped to prevent XSS attacks
|
||||
- **XSS-Safe DOM Construction**: JavaScript uses `createElement()` and `textContent` instead of `innerHTML`
|
||||
- **CSRF Protection**: Nonce verification on all forms and AJAX requests
|
||||
- **SQL Injection Prevention**: All database queries use prepared statements
|
||||
- **Capability Checks**: Admin functions require `manage_woocommerce` capability
|
||||
- **Secure Downloads**: File downloads use hash-verified URLs with user authentication
|
||||
- **Response Signing**: Optional HMAC-SHA256 signatures for API tamper protection
|
||||
- **Rate Limiting**: API and frontend operations protected against abuse
|
||||
- **Import Limits**: CSV imports limited by file size, row count, and cooldown period
|
||||
|
||||
### Trusted Proxy Configuration
|
||||
|
||||
@@ -330,6 +342,41 @@ Content-Type: application/json
|
||||
| `max_activations_reached` | Maximum activations reached |
|
||||
| `rate_limit_exceeded` | Too many requests (wait and retry) |
|
||||
|
||||
## Auto-Updates
|
||||
|
||||
Licensed plugins can receive updates through WordPress's native plugin update system. When properly configured, WordPress will check the license server for updates and display them in the Plugins page.
|
||||
|
||||
### Configuration
|
||||
|
||||
In WooCommerce > Settings > Licensed Products > Auto-Updates:
|
||||
|
||||
- **Enable Update Notifications**: Show available updates in WordPress admin
|
||||
- **Automatically Install Updates**: Let WordPress install updates automatically
|
||||
- **Update Check Frequency**: How often to check for updates (1-168 hours)
|
||||
|
||||
### How It Works
|
||||
|
||||
1. The plugin periodically checks the configured license server for updates
|
||||
2. If a newer version is available and the license is valid, WordPress shows the update
|
||||
3. Updates can be installed manually or automatically (if enabled)
|
||||
4. Downloads are authenticated using the license key
|
||||
|
||||
### API Endpoint
|
||||
|
||||
The update check uses the `/update-check` REST API endpoint:
|
||||
|
||||
```http
|
||||
POST /wp-json/wc-licensed-product/v1/update-check
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||
"domain": "example.com",
|
||||
"plugin_slug": "my-plugin",
|
||||
"current_version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## License Statuses
|
||||
|
||||
- **Active**: License is valid and usable
|
||||
@@ -359,6 +406,60 @@ For issues and feature requests, please visit:
|
||||
|
||||
Marco Graetsch
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
After cloning the repository, initialize the git submodule and install dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://src.bundespruefstelle.ch/magdev/wc-licensed-product.git
|
||||
cd wc-licensed-product
|
||||
git submodule update --init --recursive
|
||||
composer install
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `src/` - PHP source files (PSR-4 autoloaded)
|
||||
- `assets/` - CSS and JavaScript files
|
||||
- `templates/` - Twig templates for admin and frontend views
|
||||
- `languages/` - Translation files (.pot, .po, .mo)
|
||||
- `lib/` - Git submodule for the client library
|
||||
- `docs/` - API documentation and client examples
|
||||
|
||||
### Creating Releases
|
||||
|
||||
Releases are automatically created by the Gitea CI/CD pipeline when a version tag is pushed:
|
||||
|
||||
```bash
|
||||
# Update version in wc-licensed-product.php (both header and constant)
|
||||
# Update CHANGELOG.md with release notes
|
||||
git add -A && git commit -m "Release v0.7.3"
|
||||
git tag -a v0.7.3 -m "Release v0.7.3"
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
The pipeline will:
|
||||
|
||||
1. Build production dependencies
|
||||
2. Compile translations
|
||||
3. Create the release package with proper WordPress structure
|
||||
4. Generate SHA256 checksum
|
||||
5. Publish to Gitea releases
|
||||
|
||||
### Translations
|
||||
|
||||
To add or update translations:
|
||||
|
||||
```bash
|
||||
# Extract strings to .pot template
|
||||
# (Use a tool like wp-cli or poedit)
|
||||
|
||||
# Compile .po files to .mo for production
|
||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later
|
||||
|
||||
@@ -12,14 +12,17 @@
|
||||
],
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git"
|
||||
"type": "path",
|
||||
"url": "lib/wc-licensed-product-client",
|
||||
"options": {
|
||||
"symlink": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3.0",
|
||||
"twig/twig": "^3.0",
|
||||
"magdev/wc-licensed-product-client": "dev-main"
|
||||
"magdev/wc-licensed-product-client": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
27
composer.lock
generated
27
composer.lock
generated
@@ -4,15 +4,15 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "05af8ab515abe7e689c610724b54e27a",
|
||||
"content-hash": "f13b7ed9531068d0180f28adc8a80397",
|
||||
"packages": [
|
||||
{
|
||||
"name": "magdev/wc-licensed-product-client",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
||||
"reference": "760e1e752a0c088fa634cf7ff678e0735ed525a4"
|
||||
"dist": {
|
||||
"type": "path",
|
||||
"url": "lib/wc-licensed-product-client",
|
||||
"reference": "f9281ec5fb23bf1993ab0240e0347c835009a10f"
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
@@ -24,7 +24,6 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -52,7 +51,9 @@
|
||||
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
||||
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
||||
},
|
||||
"time": "2026-01-27T19:52:12+00:00"
|
||||
"transport-options": {
|
||||
"relative": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
@@ -380,16 +381,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.4",
|
||||
"version": "v7.4.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "d63c23357d74715a589454c141c843f0172bec6c"
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
|
||||
"reference": "d63c23357d74715a589454c141c843f0172bec6c",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -457,7 +458,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -477,7 +478,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-23T16:34:22+00:00"
|
||||
"time": "2026-01-27T16:16:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
|
||||
@@ -21,7 +21,7 @@ This prevents attackers from:
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 7.4+ (8.0+ recommended)
|
||||
- PHP 8.3+
|
||||
- A server secret stored securely (not in version control)
|
||||
|
||||
## Server Configuration
|
||||
@@ -51,25 +51,33 @@ php -r "echo bin2hex(random_bytes(32));"
|
||||
|
||||
### Key Derivation
|
||||
|
||||
Each license key gets a unique signing key derived from the server secret:
|
||||
Each license key gets a unique signing key derived from the server secret using RFC 5869 HKDF:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Derive a unique signing key for a license.
|
||||
*
|
||||
* @param string $licenseKey The license key
|
||||
* @param string $serverSecret The server's master secret
|
||||
* @return string The derived key (hex encoded)
|
||||
* Uses PHP's native hash_hkdf() function per RFC 5869.
|
||||
*
|
||||
* @param string $licenseKey The license key (used as "info" context)
|
||||
* @param string $serverSecret The server's master secret (used as IKM)
|
||||
* @return string The derived key (hex encoded, 64 characters)
|
||||
*/
|
||||
function derive_signing_key(string $licenseKey, string $serverSecret): string
|
||||
{
|
||||
// HKDF-like key derivation
|
||||
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
|
||||
|
||||
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
|
||||
// HKDF key derivation per RFC 5869
|
||||
// IKM: server_secret, Length: 32 bytes, Info: license_key
|
||||
return bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey));
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** This uses PHP's native `hash_hkdf()` function (available since PHP 7.1.2). The parameters are:
|
||||
|
||||
- **Algorithm:** sha256
|
||||
- **IKM (Input Keying Material):** server_secret
|
||||
- **Length:** 32 bytes (256 bits)
|
||||
- **Info:** license_key (context-specific information)
|
||||
|
||||
### Response Signing
|
||||
|
||||
Sign every API response before sending:
|
||||
@@ -88,8 +96,8 @@ function sign_response(array $responseData, string $licenseKey, string $serverSe
|
||||
$timestamp = time();
|
||||
$signingKey = derive_signing_key($licenseKey, $serverSecret);
|
||||
|
||||
// Sort keys for consistent ordering
|
||||
ksort($responseData);
|
||||
// Recursively sort keys for consistent ordering (important for nested arrays!)
|
||||
$responseData = recursive_key_sort($responseData);
|
||||
|
||||
// Build signature payload
|
||||
$jsonBody = json_encode($responseData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
@@ -103,6 +111,20 @@ function sign_response(array $responseData, string $licenseKey, string $serverSe
|
||||
'X-License-Timestamp' => (string) $timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sort array keys alphabetically.
|
||||
*/
|
||||
function recursive_key_sort(array $data): array
|
||||
{
|
||||
ksort($data);
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$data[$key] = recursive_key_sort($value);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
### WordPress REST API Integration
|
||||
@@ -214,7 +236,7 @@ class ResponseSigner
|
||||
$timestamp = time();
|
||||
$signingKey = $this->deriveKey($licenseKey);
|
||||
|
||||
ksort($data);
|
||||
$data = $this->recursiveKeySort($data);
|
||||
$payload = $timestamp . ':' . json_encode(
|
||||
$data,
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
||||
@@ -226,11 +248,21 @@ class ResponseSigner
|
||||
];
|
||||
}
|
||||
|
||||
private function recursiveKeySort(array $data): array
|
||||
{
|
||||
ksort($data);
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$data[$key] = $this->recursiveKeySort($value);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function deriveKey(string $licenseKey): string
|
||||
{
|
||||
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
|
||||
|
||||
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
|
||||
// HKDF key derivation per RFC 5869
|
||||
return bin2hex(hash_hkdf('sha256', $this->serverSecret, 32, $licenseKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +294,8 @@ signature = HMAC-SHA256(
|
||||
|
||||
Where:
|
||||
|
||||
- `derive_signing_key` uses HKDF-like derivation (see above)
|
||||
- `canonical_json` sorts keys alphabetically, no escaping of slashes/unicode
|
||||
- `derive_signing_key` uses RFC 5869 HKDF: `hash_hkdf('sha256', server_secret, 32, license_key)`
|
||||
- `canonical_json` recursively sorts keys alphabetically, no escaping of slashes/unicode
|
||||
- Result is hex-encoded (64 characters)
|
||||
|
||||
## Testing
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
lib/wc-licensed-product-client
Submodule
1
lib/wc-licensed-product-client
Submodule
Submodule lib/wc-licensed-product-client added at 56abe8a97c
BIN
releases/wc-licensed-product-0.7.1.zip
Normal file
BIN
releases/wc-licensed-product-0.7.1.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.7.1.zip.sha256
Normal file
1
releases/wc-licensed-product-0.7.1.zip.sha256
Normal file
@@ -0,0 +1 @@
|
||||
6ffd0bdf47395436bbc28a029eff4c6d065f2b5b64c687b96ae36a74c3ee34ef wc-licensed-product-0.7.1.zip
|
||||
@@ -79,7 +79,8 @@ final class ResponseSigner
|
||||
|
||||
return str_starts_with($route, '/wc-licensed-product/v1/validate')
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/status')
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/activate');
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/activate')
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/update-check');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -428,6 +428,26 @@ final class AccountController
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (ResponseSigner::isSigningEnabled() && !empty($license['customer_secret'])): ?>
|
||||
<div class="license-row-secret">
|
||||
<button type="button" class="secret-toggle" aria-expanded="false">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php esc_html_e('API Verification Secret', 'wc-licensed-product'); ?>
|
||||
<span class="dashicons dashicons-arrow-down-alt2 toggle-arrow"></span>
|
||||
</button>
|
||||
<div class="secret-content" style="display: none;">
|
||||
<p class="secret-description">
|
||||
<?php esc_html_e('Use this secret to verify signed API responses. Keep it secure.', 'wc-licensed-product'); ?>
|
||||
</p>
|
||||
<div class="secret-value-wrapper">
|
||||
<code class="secret-value"><?php echo esc_html($license['customer_secret']); ?></code>
|
||||
<button type="button" class="copy-secret-btn" data-secret="<?php echo esc_attr($license['customer_secret']); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-clipboard"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -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.0
|
||||
* Version: 0.7.2
|
||||
* 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.0');
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.7.2');
|
||||
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