3 Commits

Author SHA1 Message Date
f3cd19efe0 feat: Add license management and tabbed settings (v0.3.0)
- Implement license management using magdev/wc-licensed-product-client
- Reorganize settings page into License, Default Settings, Integrations tabs
- Add license validation and activation via AJAX
- Frontend features require valid license (admin works always)
- Update translations with German (de_CH) for license strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 12:03:05 +01:00
d62f01cf41 docs: Update session history with v0.1.1 and v0.2.0 work
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:56:52 +01:00
edfd19dea1 docs: Fix markdown linting issues in user guide
- Consistent table column separator widths
- Rename duplicate "Requirements" headings to be more specific

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:56:13 +01:00
16 changed files with 6580 additions and 111 deletions

View File

@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.3.0] - 2026-01-29
### Added
- License management integration using `magdev/wc-licensed-product-client` package
- Tabbed settings page with License, Default Settings, and Integrations tabs
- License validation and activation via AJAX with real-time status updates
- License status banner showing current license state and expiration
- License checks for frontend components (unlicensed sites show message instead of content)
### Changed
- Reorganized settings page into three tabs for better organization
- Frontend features (player, shortcodes, ActivityPub) now require valid license
- Admin/backend functionality works regardless of license status
### Security
- Server secret stored securely in WordPress options
- HMAC signature verification for license server responses
- Nonce verification for all license AJAX operations
## [0.2.0] - 2026-01-28 ## [0.2.0] - 2026-01-28
### Added ### Added

View File

@@ -24,9 +24,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. **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.2.1 (Bugfix) (No pending features - all roadmap items completed)
### Version 0.3.0 (Minor)
## Technical Stack ## Technical Stack
@@ -397,3 +395,72 @@ wp-fedistream/
- Successfully pushed dev, main branches and v0.1.0 tag to origin - Successfully pushed dev, main branches and v0.1.0 tag to origin
- Remote URL updated from HTTPS to SSH for authentication - Remote URL updated from HTTPS to SSH for authentication
- First release is now live at the repository - First release is now live at the repository
### 2026-01-28 - Bugfix v0.1.1 and Feature v0.2.0
**Summary:** Fixed WooCommerce integration timing bug, added plugin action links and user guide.
**v0.1.1 - Bugfix:**
- Fixed WooCommerce product types not appearing in product selector
- Root cause: `Integration` constructor hooked `check_woocommerce` to `plugins_loaded` priority 5, but class was instantiated at priority 10 (too late)
- Solution: Call `check_woocommerce()` directly in constructor
**v0.2.0 - Features:**
- Added Dashboard and Settings links to WordPress Plugins page
- Created comprehensive `USERGUIDE.md` covering all features
**Files Modified:**
- `includes/WooCommerce/Integration.php` - Fixed hook timing
- `includes/Plugin.php` - Added `add_plugin_action_links()` method
**Files Created:**
- `USERGUIDE.md` - Comprehensive user documentation
**Notes:**
- All releases pushed to origin (v0.1.1 and v0.2.0 tags)
- Markdown linting fixes applied to USERGUIDE.md
### 2026-01-29 - License Management v0.3.0
**Summary:** Implemented license management integration and reorganized settings page into tabs.
**Features:**
- License management using `magdev/wc-licensed-product-client` package
- Tabbed settings page: License, Default Settings, Integrations
- License validation and activation via AJAX
- License status banner with expiration display
- Frontend license checks (unlicensed sites show message instead of content)
- Admin/backend works regardless of license status
**License Behavior:**
- Backend (admin): Full access always
- Frontend (player, shortcodes, ActivityPub): Requires valid license
**Files Created:**
- `includes/License/Manager.php` - License management wrapper class
**Files Modified:**
- `composer.json` - Added VCS repository and `magdev/wc-licensed-product-client` dependency
- `includes/Plugin.php` - Tabbed settings page, license manager initialization, conditional frontend loading
- `includes/Installer.php` - Added default license options
- `includes/Frontend/Shortcodes.php` - Added unlicensed mode support
- `includes/Frontend/Ajax.php` - Added license checks to public AJAX endpoints
- `assets/js/admin.js` - License validation AJAX handlers
- `assets/css/admin.css` - Tab and license status styling
- `wp-fedistream.php` - Version bump to 0.3.0
- `CHANGELOG.md` - Added v0.3.0 entry
**Notes:**
- Package name is `magdev/wc-licensed-product-client` (not `wc-license-product-client`)
- Uses Symfony HTTP Client via the license client package
- License validation cached for 24 hours using WordPress transients

View File

@@ -348,7 +348,7 @@ FediStream includes widgets for your sidebar or widget areas.
FediStream integrates with the Fediverse through ActivityPub, allowing your artists to be followed from Mastodon, Pixelfed, and other platforms. FediStream integrates with the Fediverse through ActivityPub, allowing your artists to be followed from Mastodon, Pixelfed, and other platforms.
### Requirements ### Requirements for ActivityPub integration
Install and activate the [ActivityPub plugin](https://wordpress.org/plugins/activitypub/) from WordPress.org. Install and activate the [ActivityPub plugin](https://wordpress.org/plugins/activitypub/) from WordPress.org.
@@ -386,7 +386,7 @@ Artist followers are displayed on the artist's admin page under the Followers me
Sell your music directly through your WordPress site using WooCommerce. Sell your music directly through your WordPress site using WooCommerce.
### Requirements ### Requirements for WooCommerce Integration
- WooCommerce 10.0 or higher installed and activated - WooCommerce 10.0 or higher installed and activated
- WooCommerce integration enabled in FediStream settings - WooCommerce integration enabled in FediStream settings

View File

@@ -4,4 +4,122 @@
* @package WP_FediStream * @package WP_FediStream
*/ */
/* Admin styles will be added here */ /* Settings page tabs */
.nav-tab-wrapper + .fedistream-settings-content {
margin-top: -1px;
}
.fedistream-settings-content {
background: #fff;
border: 1px solid #c3c4c7;
border-top: none;
padding: 20px;
}
/* Active tab styling */
.wrap .nav-tab-wrapper .nav-tab-active {
background: #fff;
border-bottom-color: #fff;
}
/* License status banner */
.fedistream-license-status {
margin-bottom: 20px;
}
.fedistream-license-status .dashicons {
color: inherit;
}
.fedistream-license-status.notice-success .dashicons {
color: #00a32a;
}
.fedistream-license-status.notice-error .dashicons {
color: #d63638;
}
.fedistream-license-status.notice-warning .dashicons {
color: #dba617;
}
.fedistream-license-status.notice-info .dashicons {
color: #72aee6;
}
/* License form buttons */
#fedistream-license-form .button .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
line-height: 1.3;
}
/* License message display */
#fedistream-license-message {
padding: 10px 15px;
}
#fedistream-license-message p {
margin: 0;
}
/* Dashboard stats grid */
.fedistream-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 20px 0;
}
.fedistream-stat-box {
background: #fff;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 4px;
}
.fedistream-stat-box h3 {
margin: 0 0 10px;
}
.fedistream-stat-box p {
font-size: 2em;
margin: 0;
color: #2271b1;
}
/* Quick actions */
.fedistream-quick-actions {
background: #fff;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 4px;
margin: 20px 0;
}
/* Info box */
.fedistream-info {
background: #fff;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 4px;
}
/* Responsive adjustments */
@media screen and (max-width: 782px) {
.fedistream-stats {
grid-template-columns: repeat(2, 1fr);
}
#fedistream-license-form .button {
display: block;
margin: 10px 0 0 0 !important;
}
}
@media screen and (max-width: 480px) {
.fedistream-stats {
grid-template-columns: 1fr;
}
}

View File

@@ -8,7 +8,115 @@
'use strict'; 'use strict';
$(document).ready(function() { $(document).ready(function() {
// Admin scripts will be added here // License validation functionality
initLicenseValidation();
}); });
/**
* Initialize license validation AJAX handlers.
*/
function initLicenseValidation() {
var $validateBtn = $('#fedistream-validate-license');
var $activateBtn = $('#fedistream-activate-license');
var $spinner = $('#fedistream-license-spinner');
var $message = $('#fedistream-license-message');
if (!$validateBtn.length) {
return;
}
// Validate license button
$validateBtn.on('click', function(e) {
e.preventDefault();
performLicenseAction('fedistream_validate_license', 'Validating...');
});
// Activate license button
$activateBtn.on('click', function(e) {
e.preventDefault();
performLicenseAction('fedistream_activate_license', 'Activating...');
});
/**
* Perform license AJAX action.
*
* @param {string} action AJAX action name.
* @param {string} loadingText Loading button text.
*/
function performLicenseAction(action, loadingText) {
var originalText = $validateBtn.text();
// Show loading state
$spinner.addClass('is-active');
$validateBtn.prop('disabled', true);
$activateBtn.prop('disabled', true);
$message.hide();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: action,
nonce: fedistreamLicenseNonce
},
success: function(response) {
$spinner.removeClass('is-active');
$validateBtn.prop('disabled', false);
$activateBtn.prop('disabled', false);
if (response.success) {
showMessage('success', response.data.message);
// Reload page to show updated status
setTimeout(function() {
window.location.reload();
}, 1500);
} else {
showMessage('error', response.data.message || 'An error occurred.');
}
},
error: function(xhr, status, error) {
$spinner.removeClass('is-active');
$validateBtn.prop('disabled', false);
$activateBtn.prop('disabled', false);
showMessage('error', 'Request failed. Please try again.');
}
});
}
/**
* Show a message to the user.
*
* @param {string} type Message type: 'success', 'error', 'warning', 'info'.
* @param {string} text Message text.
*/
function showMessage(type, text) {
var classMap = {
'success': 'notice-success',
'error': 'notice-error',
'warning': 'notice-warning',
'info': 'notice-info'
};
var noticeClass = classMap[type] || 'notice-info';
$message
.removeClass('notice-success notice-error notice-warning notice-info')
.addClass('notice ' + noticeClass)
.html('<p>' + escapeHtml(text) + '</p>')
.show();
}
/**
* Escape HTML entities.
*
* @param {string} text Text to escape.
* @return {string} Escaped text.
*/
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
})(jQuery); })(jQuery);

View File

@@ -14,8 +14,15 @@
"support": { "support": {
"issues": "https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues" "issues": "https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues"
}, },
"repositories": [
{
"type": "vcs",
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git"
}
],
"require": { "require": {
"php": ">=8.3", "php": ">=8.3",
"magdev/wc-licensed-product-client": "^0.1",
"twig/twig": "^3.0" "twig/twig": "^3.0"
}, },
"require-dev": { "require-dev": {

652
composer.lock generated
View File

@@ -4,8 +4,312 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c8fb50541e5730c8ad92b76392765aca", "content-hash": "29e8e4e069b25dee0a610019a77dab50",
"packages": [ "packages": [
{
"name": "magdev/wc-licensed-product-client",
"version": "v0.1.0",
"source": {
"type": "git",
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
"reference": "83037ea0c2d9e365cf9ec0ad50251d3ebc7e4782"
},
"require": {
"php": "^8.3",
"psr/cache": "^3.0",
"psr/http-client": "^1.0",
"psr/log": "^3.0",
"symfony/http-client": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Magdev\\WcLicensedProductClient\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Magdev\\WcLicensedProductClient\\Tests\\": "tests/"
}
},
"license": [
"GPL-2.0-or-later"
],
"authors": [
{
"name": "Marco Graetsch",
"email": "magdev3.0@gmail.com",
"homepage": "https://src.bundespruefstelle.ch/magdev"
}
],
"description": "Client library for WooCommerce Licensed Product Plugin - Activate, validate and check the status of licenses via REST API",
"homepage": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client",
"support": {
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
},
"time": "2026-01-22T15:24:57+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"time": "2024-09-11T13:17:53+00:00"
},
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.6.0", "version": "v3.6.0",
@@ -73,6 +377,185 @@
], ],
"time": "2024-09-25T14:21:43+00:00" "time": "2024-09-25T14:21:43+00:00"
}, },
{
"name": "symfony/http-client",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/polyfill-php83": "^1.29",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"amphp/amp": "<2.5",
"amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/amphp-http-client-meta": "^1.0|^2.0",
"symfony/cache": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/messenger": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
"symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-29T11:18:49+00:00"
},
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.33.0", "version": "v1.33.0",
@@ -241,6 +724,173 @@
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2024-12-23T08:48:59+00:00"
}, },
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php83\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-07-08T02:45:35+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-07-15T11:30:57+00:00"
},
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.23.0", "version": "v3.23.0",

View File

@@ -7,6 +7,8 @@
namespace WP_FediStream\Frontend; namespace WP_FediStream\Frontend;
use WP_FediStream\License\Manager as LicenseManager;
// Prevent direct file access. // Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
@@ -36,6 +38,11 @@ class Ajax {
* @return void * @return void
*/ */
public function get_track(): void { public function get_track(): void {
// Check license.
if ( ! LicenseManager::is_license_valid() ) {
wp_send_json_error( array( 'message' => __( 'This feature requires a valid license.', 'wp-fedistream' ) ) );
}
// Verify nonce. // Verify nonce.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) ); wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
@@ -125,6 +132,11 @@ class Ajax {
* @return void * @return void
*/ */
public function record_play(): void { public function record_play(): void {
// Check license.
if ( ! LicenseManager::is_license_valid() ) {
wp_send_json_error( array( 'message' => __( 'This feature requires a valid license.', 'wp-fedistream' ) ) );
}
// Verify nonce. // Verify nonce.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) ); wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );

View File

@@ -27,13 +27,35 @@ class Shortcodes {
private Plugin $plugin; private Plugin $plugin;
/** /**
* Constructor. * Whether running in unlicensed mode.
*
* @var bool
*/ */
public function __construct() { private bool $unlicensed_mode = false;
/**
* Constructor.
*
* @param bool $unlicensed_mode Whether to run in unlicensed mode.
*/
public function __construct( bool $unlicensed_mode = false ) {
$this->plugin = Plugin::get_instance(); $this->plugin = Plugin::get_instance();
$this->unlicensed_mode = $unlicensed_mode;
$this->register_shortcodes(); $this->register_shortcodes();
} }
/**
* Get the unlicensed message HTML.
*
* @return string
*/
private function get_unlicensed_message(): string {
return '<div class="fedistream-unlicensed-notice" style="padding: 20px; background: #f0f0f1; border-left: 4px solid #dba617; margin: 10px 0;">'
. '<p style="margin: 0; color: #50575e;">'
. esc_html__( 'This content requires a valid FediStream license.', 'wp-fedistream' )
. '</p></div>';
}
/** /**
* Register all shortcodes. * Register all shortcodes.
* *
@@ -501,6 +523,11 @@ class Shortcodes {
* @return string * @return string
*/ */
private function render_template( string $template, array $context ): string { private function render_template( string $template, array $context ): string {
// Check for unlicensed mode.
if ( $this->unlicensed_mode ) {
return $this->get_unlicensed_message();
}
try { try {
return $this->plugin->render( $template, $context ); return $this->plugin->render( $template, $context );
} catch ( \Exception $e ) { } catch ( \Exception $e ) {

View File

@@ -350,6 +350,13 @@ class Installer {
'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ), 'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
'wp_fedistream_max_upload_size' => 50, // MB 'wp_fedistream_max_upload_size' => 50, // MB
'wp_fedistream_default_license' => 'all-rights-reserved', 'wp_fedistream_default_license' => 'all-rights-reserved',
// License management options.
'wp_fedistream_license_key' => '',
'wp_fedistream_license_server_url' => '',
'wp_fedistream_license_server_secret' => '',
'wp_fedistream_license_status' => 'unchecked',
'wp_fedistream_license_data' => array(),
'wp_fedistream_license_last_check' => 0,
); );
foreach ( $defaults as $option => $value ) { foreach ( $defaults as $option => $value ) {

View File

@@ -0,0 +1,653 @@
<?php
/**
* License management class.
*
* Wraps the wc-licensed-product-client library for WordPress integration.
*
* @package WP_FediStream
*/
namespace WP_FediStream\License;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Dto\LicenseInfo;
use Magdev\WcLicensedProductClient\Dto\LicenseStatus;
use Magdev\WcLicensedProductClient\Dto\LicenseState;
use Magdev\WcLicensedProductClient\Dto\ActivationResult;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Magdev\WcLicensedProductClient\Exception\LicenseNotFoundException;
use Magdev\WcLicensedProductClient\Exception\LicenseExpiredException;
use Magdev\WcLicensedProductClient\Exception\LicenseInvalidException;
use Magdev\WcLicensedProductClient\Exception\LicenseRevokedException;
use Magdev\WcLicensedProductClient\Exception\LicenseInactiveException;
use Magdev\WcLicensedProductClient\Exception\DomainMismatchException;
use Magdev\WcLicensedProductClient\Exception\MaxActivationsReachedException;
use Magdev\WcLicensedProductClient\Exception\RateLimitExceededException;
use Magdev\WcLicensedProductClient\Security\SignatureException;
use Symfony\Component\HttpClient\HttpClient;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* License Manager class.
*
* Handles license validation, activation, and status checking.
*/
final class Manager {
/**
* Option names for license settings.
*/
public const OPTION_LICENSE_KEY = 'wp_fedistream_license_key';
public const OPTION_SERVER_URL = 'wp_fedistream_license_server_url';
public const OPTION_SERVER_SECRET = 'wp_fedistream_license_server_secret';
public const OPTION_LICENSE_STATUS = 'wp_fedistream_license_status';
public const OPTION_LICENSE_DATA = 'wp_fedistream_license_data';
public const OPTION_LAST_CHECK = 'wp_fedistream_license_last_check';
/**
* Transient name for caching license validation.
*/
private const TRANSIENT_LICENSE_CHECK = 'wp_fedistream_license_check';
/**
* Cache TTL in seconds (24 hours).
*/
private const CACHE_TTL = 86400;
/**
* Singleton instance.
*
* @var Manager|null
*/
private static ?Manager $instance = null;
/**
* License client instance.
*
* @var SecureLicenseClient|null
*/
private ?SecureLicenseClient $client = null;
/**
* Get singleton instance.
*
* @return Manager
*/
public static function get_instance(): Manager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor.
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'wp_ajax_fedistream_validate_license', array( $this, 'ajax_validate_license' ) );
add_action( 'wp_ajax_fedistream_activate_license', array( $this, 'ajax_activate_license' ) );
add_action( 'wp_ajax_fedistream_deactivate_license', array( $this, 'ajax_deactivate_license' ) );
add_action( 'wp_ajax_fedistream_check_license_status', array( $this, 'ajax_check_status' ) );
}
/**
* Initialize the license client.
*
* @return bool True if client was initialized successfully.
*/
private function init_client(): bool {
if ( null !== $this->client ) {
return true;
}
$server_url = self::get_server_url();
$server_secret = self::get_server_secret();
if ( empty( $server_url ) || empty( $server_secret ) ) {
return false;
}
try {
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $server_url,
serverSecret: $server_secret,
);
return true;
} catch ( \Throwable $e ) {
return false;
}
}
/**
* Validate the current license.
*
* @return array{success: bool, message: string, data?: array}
*/
public function validate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-fedistream' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->validate( $license_key, $domain );
// Update cached status.
$this->update_cached_status( 'valid', array(
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'c' ),
'version_id' => $result->versionId,
) );
return array(
'success' => true,
'message' => __( 'License validated successfully.', 'wp-fedistream' ),
'data' => array(
'status' => 'valid',
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
'lifetime' => $result->isLifetime(),
),
);
} catch ( LicenseNotFoundException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'License key not found. Please check your license key.', 'wp-fedistream' ),
);
} catch ( LicenseExpiredException $e ) {
$this->update_cached_status( 'expired' );
return array(
'success' => false,
'message' => __( 'Your license has expired. Please renew to continue.', 'wp-fedistream' ),
);
} catch ( LicenseRevokedException $e ) {
$this->update_cached_status( 'revoked' );
return array(
'success' => false,
'message' => __( 'Your license has been revoked.', 'wp-fedistream' ),
);
} catch ( LicenseInactiveException $e ) {
$this->update_cached_status( 'inactive' );
return array(
'success' => false,
'message' => __( 'License is inactive. Please activate it first.', 'wp-fedistream' ),
);
} catch ( DomainMismatchException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'This license is not activated for this domain.', 'wp-fedistream' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
);
} catch ( RateLimitExceededException $e ) {
return array(
'success' => false,
'message' => __( 'Too many requests. Please try again later.', 'wp-fedistream' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License validation failed: %s', 'wp-fedistream' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to verify license. Please try again later.', 'wp-fedistream' ),
);
}
}
/**
* Activate the license for this domain.
*
* @return array{success: bool, message: string, data?: array}
*/
public function activate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-fedistream' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->activate( $license_key, $domain );
if ( $result->success ) {
// Validate after activation to get full license info.
return $this->validate();
}
return array(
'success' => false,
'message' => $result->message,
);
} catch ( MaxActivationsReachedException $e ) {
return array(
'success' => false,
'message' => __( 'Maximum number of activations reached. Please deactivate another site first.', 'wp-fedistream' ),
);
} catch ( LicenseNotFoundException $e ) {
return array(
'success' => false,
'message' => __( 'License key not found. Please check your license key.', 'wp-fedistream' ),
);
} catch ( LicenseExpiredException $e ) {
return array(
'success' => false,
'message' => __( 'Your license has expired. Please renew to continue.', 'wp-fedistream' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License activation failed: %s', 'wp-fedistream' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to activate license. Please try again later.', 'wp-fedistream' ),
);
}
}
/**
* Get the current license status.
*
* @param bool $force_refresh Force a fresh check from the server.
* @return array{success: bool, message: string, data?: array}
*/
public function get_status( bool $force_refresh = false ): array {
// Check cached status first.
if ( ! $force_refresh ) {
$cached = $this->get_cached_validation();
if ( null !== $cached ) {
return $cached;
}
}
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
'data' => array(
'status' => 'unconfigured',
),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key configured.', 'wp-fedistream' ),
'data' => array(
'status' => 'unchecked',
),
);
}
try {
$result = $this->client->status( $license_key );
$status_map = array(
LicenseState::Active->value => 'valid',
LicenseState::Inactive->value => 'inactive',
LicenseState::Expired->value => 'expired',
LicenseState::Revoked->value => 'revoked',
);
$status = $status_map[ $result->status->value ] ?? 'invalid';
$data = array(
'status' => $status,
'valid' => $result->valid,
'domain' => $result->domain,
'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
'lifetime' => $result->isLifetime(),
'activations_count' => $result->activationsCount,
'max_activations' => $result->maxActivations,
);
// Cache the result.
$this->cache_validation( array(
'success' => $result->valid,
'message' => $result->valid
? __( 'License is active.', 'wp-fedistream' )
: __( 'License is not active.', 'wp-fedistream' ),
'data' => $data,
) );
$this->update_cached_status( $status, $data );
return array(
'success' => $result->valid,
'message' => $result->valid
? __( 'License is active.', 'wp-fedistream' )
: __( 'License is not active.', 'wp-fedistream' ),
'data' => $data,
);
} catch ( LicenseNotFoundException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'License key not found.', 'wp-fedistream' ),
'data' => array(
'status' => 'invalid',
),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
'data' => array(
'status' => 'error',
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to check license status.', 'wp-fedistream' ),
'data' => array(
'status' => 'error',
),
);
}
}
/**
* Deactivate the license (clear local data).
*
* @return bool
*/
public function deactivate(): bool {
self::clear_license_data();
return true;
}
/**
* Check if the license is currently valid.
*
* Uses cached status for performance.
*
* @return bool
*/
public static function is_license_valid(): bool {
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
return 'valid' === $status;
}
/**
* Get the license key.
*
* @return string
*/
public static function get_license_key(): string {
return get_option( self::OPTION_LICENSE_KEY, '' );
}
/**
* Get the license server URL.
*
* @return string
*/
public static function get_server_url(): string {
return get_option( self::OPTION_SERVER_URL, '' );
}
/**
* Get the server secret.
*
* @return string
*/
public static function get_server_secret(): string {
return get_option( self::OPTION_SERVER_SECRET, '' );
}
/**
* Get cached license status.
*
* @return string
*/
public static function get_cached_status(): string {
return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
}
/**
* Get cached license data.
*
* @return array
*/
public static function get_cached_data(): array {
return get_option( self::OPTION_LICENSE_DATA, array() );
}
/**
* Get last check timestamp.
*
* @return int
*/
public static function get_last_check(): int {
return (int) get_option( self::OPTION_LAST_CHECK, 0 );
}
/**
* Save license settings.
*
* @param array $data Settings data.
* @return bool
*/
public static function save_settings( array $data ): bool {
if ( isset( $data['license_key'] ) ) {
update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) );
}
if ( isset( $data['server_url'] ) ) {
update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) );
}
if ( isset( $data['server_secret'] ) ) {
// Only update if a new secret is provided.
$secret = sanitize_text_field( $data['server_secret'] );
if ( ! empty( $secret ) ) {
update_option( self::OPTION_SERVER_SECRET, $secret );
}
}
// Reset status when settings change.
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
return true;
}
/**
* Clear all license data.
*
* @return void
*/
public static function clear_license_data(): void {
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
update_option( self::OPTION_LICENSE_DATA, array() );
update_option( self::OPTION_LAST_CHECK, 0 );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
}
/**
* Update cached license status.
*
* @param string $status Status value.
* @param array $data Additional data.
* @return void
*/
private function update_cached_status( string $status, array $data = array() ): void {
update_option( self::OPTION_LICENSE_STATUS, $status );
update_option( self::OPTION_LICENSE_DATA, $data );
update_option( self::OPTION_LAST_CHECK, time() );
}
/**
* Cache validation result.
*
* @param array $result Validation result.
* @return void
*/
private function cache_validation( array $result ): void {
set_transient( self::TRANSIENT_LICENSE_CHECK, $result, self::CACHE_TTL );
}
/**
* Get cached validation result.
*
* @return array|null
*/
private function get_cached_validation(): ?array {
$cached = get_transient( self::TRANSIENT_LICENSE_CHECK );
return false === $cached ? null : $cached;
}
/**
* AJAX handler: Validate license.
*
* @return void
*/
public function ajax_validate_license(): void {
check_ajax_referer( 'fedistream_license_action', 'nonce' );
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
) );
}
$result = $this->validate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Activate license.
*
* @return void
*/
public function ajax_activate_license(): void {
check_ajax_referer( 'fedistream_license_action', 'nonce' );
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
) );
}
$result = $this->activate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Deactivate license.
*
* @return void
*/
public function ajax_deactivate_license(): void {
check_ajax_referer( 'fedistream_license_action', 'nonce' );
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
) );
}
$this->deactivate();
wp_send_json_success( array(
'success' => true,
'message' => __( 'License deactivated.', 'wp-fedistream' ),
) );
}
/**
* AJAX handler: Check license status.
*
* @return void
*/
public function ajax_check_status(): void {
check_ajax_referer( 'fedistream_license_action', 'nonce' );
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
) );
}
$force_refresh = isset( $_POST['force'] ) && 'true' === $_POST['force'];
$result = $this->get_status( $force_refresh );
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
}

View File

@@ -27,6 +27,7 @@ use WP_FediStream\Taxonomies\License;
use WP_FediStream\User\Library as UserLibrary; use WP_FediStream\User\Library as UserLibrary;
use WP_FediStream\User\LibraryPage; use WP_FediStream\User\LibraryPage;
use WP_FediStream\User\Notifications; use WP_FediStream\User\Notifications;
use WP_FediStream\License\Manager as LicenseManager;
// Prevent direct file access. // Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
@@ -155,10 +156,13 @@ final class Plugin {
new ListColumns(); new ListColumns();
} }
// Initialize frontend components. // Initialize frontend components (only if licensed).
if ( ! is_admin() ) { if ( ! is_admin() && LicenseManager::is_license_valid() ) {
new TemplateLoader(); new TemplateLoader();
new Shortcodes(); new Shortcodes();
} elseif ( ! is_admin() ) {
// Register shortcodes that show license message.
new Shortcodes( true ); // Unlicensed mode.
} }
// Initialize widgets (always needed for admin widget management). // Initialize widgets (always needed for admin widget management).
@@ -167,8 +171,8 @@ final class Plugin {
// Initialize AJAX handlers. // Initialize AJAX handlers.
new Ajax(); new Ajax();
// Initialize ActivityPub integration. // Initialize ActivityPub integration (only if licensed and enabled).
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) ) { if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) && LicenseManager::is_license_valid() ) {
new ActivityPubIntegration(); new ActivityPubIntegration();
new ActivityPubRestApi(); new ActivityPubRestApi();
} }
@@ -184,6 +188,9 @@ final class Plugin {
new UserLibrary(); new UserLibrary();
new LibraryPage(); new LibraryPage();
new Notifications(); new Notifications();
// Initialize license manager.
LicenseManager::get_instance();
} }
/** /**
@@ -409,53 +416,249 @@ final class Plugin {
return; return;
} }
// Save settings. // Get current tab.
if ( isset( $_POST['fedistream_settings_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['fedistream_settings_nonce'] ), 'fedistream_save_settings' ) ) { $current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license';
update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '</p></div>'; // Handle form submissions.
} $this->handle_settings_save( $current_tab );
// Get current settings. // Get current settings.
$enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 ); $enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 );
$enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 ); $enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 );
$max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 ); $max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 );
$default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' ); $default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' );
// License settings.
$license_key = LicenseManager::get_license_key();
$license_server_url = LicenseManager::get_server_url();
$license_status = LicenseManager::get_cached_status();
$license_data = LicenseManager::get_cached_data();
$last_check = LicenseManager::get_last_check();
$tabs = array(
'license' => __( 'License', 'wp-fedistream' ),
'settings' => __( 'Default Settings', 'wp-fedistream' ),
'integrations' => __( 'Integrations', 'wp-fedistream' ),
);
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e( 'FediStream Settings', 'wp-fedistream' ); ?></h1> <h1><?php esc_html_e( 'FediStream Settings', 'wp-fedistream' ); ?></h1>
<form method="post" action=""> <nav class="nav-tab-wrapper wp-clearfix">
<?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=fedistream-settings&tab=' . $tab_key ) ); ?>"
class="nav-tab <?php echo $current_tab === $tab_key ? 'nav-tab-active' : ''; ?>">
<?php echo esc_html( $tab_label ); ?>
</a>
<?php endforeach; ?>
</nav>
<div class="fedistream-settings-content">
<?php
switch ( $current_tab ) {
case 'license':
$this->render_license_tab( $license_key, $license_server_url, $license_status, $license_data, $last_check );
break;
case 'settings':
$this->render_settings_tab( $max_upload_size, $default_license );
break;
case 'integrations':
$this->render_integrations_tab( $enable_activitypub, $enable_woocommerce );
break;
}
?>
</div>
</div>
<?php
}
/**
* Handle settings form submission.
*
* @param string $tab Current tab.
* @return void
*/
private function handle_settings_save( string $tab ): void {
if ( ! isset( $_POST['fedistream_settings_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['fedistream_settings_nonce'] ), 'fedistream_save_settings' ) ) {
return;
}
switch ( $tab ) {
case 'license':
LicenseManager::save_settings( array(
'license_key' => isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '',
'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '',
'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '',
) );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'License settings saved.', 'wp-fedistream' ) . '</p></div>';
break;
case 'settings':
update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '</p></div>';
break;
case 'integrations':
update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Integration settings saved.', 'wp-fedistream' ) . '</p></div>';
break;
}
}
/**
* Render the License tab.
*
* @param string $license_key License key.
* @param string $server_url Server URL.
* @param string $status License status.
* @param array $license_data License data.
* @param int $last_check Last check timestamp.
* @return void
*/
private function render_license_tab( string $license_key, string $server_url, string $status, array $license_data, int $last_check ): void {
$status_classes = array(
'valid' => 'notice-success',
'invalid' => 'notice-error',
'expired' => 'notice-warning',
'revoked' => 'notice-error',
'inactive' => 'notice-warning',
'unchecked' => 'notice-info',
'unconfigured' => 'notice-info',
);
$status_messages = array(
'valid' => __( 'License is active and valid.', 'wp-fedistream' ),
'invalid' => __( 'License is invalid.', 'wp-fedistream' ),
'expired' => __( 'License has expired.', 'wp-fedistream' ),
'revoked' => __( 'License has been revoked.', 'wp-fedistream' ),
'inactive' => __( 'License is inactive. Please activate it.', 'wp-fedistream' ),
'unchecked' => __( 'License has not been validated yet.', 'wp-fedistream' ),
'unconfigured' => __( 'License server is not configured.', 'wp-fedistream' ),
);
$status_icons = array(
'valid' => 'dashicons-yes-alt',
'invalid' => 'dashicons-dismiss',
'expired' => 'dashicons-warning',
'revoked' => 'dashicons-dismiss',
'inactive' => 'dashicons-marker',
'unchecked' => 'dashicons-info-outline',
'unconfigured' => 'dashicons-admin-generic',
);
$status_class = $status_classes[ $status ] ?? 'notice-info';
$status_message = $status_messages[ $status ] ?? __( 'Unknown status.', 'wp-fedistream' );
$status_icon = $status_icons[ $status ] ?? 'dashicons-info-outline';
?>
<div class="fedistream-license-status notice <?php echo esc_attr( $status_class ); ?>" style="padding: 12px; display: flex; align-items: center; gap: 10px;">
<span class="dashicons <?php echo esc_attr( $status_icon ); ?>" style="font-size: 24px; width: 24px; height: 24px;"></span>
<div>
<strong><?php echo esc_html( $status_message ); ?></strong>
<?php if ( 'valid' === $status && ! empty( $license_data['expires_at'] ) ) : ?>
<br>
<span class="description">
<?php
printf(
/* translators: %s: Expiration date */
esc_html__( 'Expires: %s', 'wp-fedistream' ),
esc_html( $license_data['expires_at'] )
);
?>
</span>
<?php elseif ( 'valid' === $status && empty( $license_data['expires_at'] ) ) : ?>
<br>
<span class="description"><?php esc_html_e( 'Lifetime license', 'wp-fedistream' ); ?></span>
<?php endif; ?>
<?php if ( $last_check > 0 ) : ?>
<br>
<span class="description">
<?php
printf(
/* translators: %s: Time ago */
esc_html__( 'Last checked: %s', 'wp-fedistream' ),
esc_html( human_time_diff( $last_check, time() ) . ' ' . __( 'ago', 'wp-fedistream' ) )
);
?>
</span>
<?php endif; ?>
</div>
</div>
<form method="post" action="" id="fedistream-license-form">
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?> <?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th scope="row"><?php esc_html_e( 'ActivityPub Integration', 'wp-fedistream' ); ?></th> <th scope="row">
<label for="license_server_url"><?php esc_html_e( 'License Server URL', 'wp-fedistream' ); ?></label>
</th>
<td> <td>
<label> <input type="url" name="license_server_url" id="license_server_url"
<input type="checkbox" name="enable_activitypub" value="1" <?php checked( $enable_activitypub, 1 ); ?>> value="<?php echo esc_attr( $server_url ); ?>"
<?php esc_html_e( 'Enable ActivityPub features', 'wp-fedistream' ); ?> class="regular-text" placeholder="https://example.com">
</label> <p class="description"><?php esc_html_e( 'The URL of your license server.', 'wp-fedistream' ); ?></p>
<p class="description"><?php esc_html_e( 'Publish releases to the Fediverse and allow followers.', 'wp-fedistream' ); ?></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row"><?php esc_html_e( 'WooCommerce Integration', 'wp-fedistream' ); ?></th> <th scope="row">
<label for="license_key"><?php esc_html_e( 'License Key', 'wp-fedistream' ); ?></label>
</th>
<td> <td>
<label> <input type="text" name="license_key" id="license_key"
<input type="checkbox" name="enable_woocommerce" value="1" <?php checked( $enable_woocommerce, 1 ); ?> <?php disabled( ! $this->is_woocommerce_active() ); ?>> value="<?php echo esc_attr( $license_key ); ?>"
<?php esc_html_e( 'Enable WooCommerce features', 'wp-fedistream' ); ?> class="regular-text" placeholder="XXXX-XXXX-XXXX-XXXX">
</label> <p class="description"><?php esc_html_e( 'Your license key from your purchase.', 'wp-fedistream' ); ?></p>
<?php if ( ! $this->is_woocommerce_active() ) : ?>
<p class="description" style="color: #d63638;"><?php esc_html_e( 'WooCommerce is not installed or active.', 'wp-fedistream' ); ?></p>
<?php else : ?>
<p class="description"><?php esc_html_e( 'Sell albums and tracks through WooCommerce.', 'wp-fedistream' ); ?></p>
<?php endif; ?>
</td> </td>
</tr> </tr>
<tr>
<th scope="row">
<label for="license_server_secret"><?php esc_html_e( 'Server Secret', 'wp-fedistream' ); ?></label>
</th>
<td>
<input type="password" name="license_server_secret" id="license_server_secret"
value="" class="regular-text" placeholder="<?php echo esc_attr( ! empty( LicenseManager::get_server_secret() ) ? '••••••••••••••••' : '' ); ?>">
<p class="description"><?php esc_html_e( '64-character verification secret from your license account. Leave empty to keep existing.', 'wp-fedistream' ); ?></p>
</td>
</tr>
</table>
<p class="submit">
<?php submit_button( __( 'Save License Settings', 'wp-fedistream' ), 'primary', 'submit', false ); ?>
<button type="button" id="fedistream-validate-license" class="button button-secondary" style="margin-left: 10px;">
<span class="dashicons dashicons-yes" style="vertical-align: middle; margin-top: -2px;"></span>
<?php esc_html_e( 'Validate License', 'wp-fedistream' ); ?>
</button>
<button type="button" id="fedistream-activate-license" class="button button-secondary" style="margin-left: 10px;">
<span class="dashicons dashicons-admin-network" style="vertical-align: middle; margin-top: -2px;"></span>
<?php esc_html_e( 'Activate License', 'wp-fedistream' ); ?>
</button>
<span id="fedistream-license-spinner" class="spinner" style="float: none; margin-top: 4px;"></span>
</p>
</form>
<div id="fedistream-license-message" style="display: none; margin-top: 10px;"></div>
<script type="text/javascript">
var fedistreamLicenseNonce = '<?php echo esc_js( wp_create_nonce( 'fedistream_license_action' ) ); ?>';
</script>
<?php
}
/**
* Render the Default Settings tab.
*
* @param int $max_upload_size Max upload size in MB.
* @param string $default_license Default license.
* @return void
*/
private function render_settings_tab( int $max_upload_size, string $default_license ): void {
?>
<form method="post" action="">
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
<table class="form-table">
<tr> <tr>
<th scope="row"> <th scope="row">
<label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label> <label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label>
@@ -485,7 +688,59 @@ final class Plugin {
<?php submit_button(); ?> <?php submit_button(); ?>
</form> </form>
</div> <?php
}
/**
* Render the Integrations tab.
*
* @param int $enable_activitypub Whether ActivityPub is enabled.
* @param int $enable_woocommerce Whether WooCommerce integration is enabled.
* @return void
*/
private function render_integrations_tab( int $enable_activitypub, int $enable_woocommerce ): void {
?>
<form method="post" action="">
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e( 'ActivityPub Integration', 'wp-fedistream' ); ?></th>
<td>
<label>
<input type="checkbox" name="enable_activitypub" value="1" <?php checked( $enable_activitypub, 1 ); ?>>
<?php esc_html_e( 'Enable ActivityPub features', 'wp-fedistream' ); ?>
</label>
<p class="description"><?php esc_html_e( 'Publish releases to the Fediverse and allow followers.', 'wp-fedistream' ); ?></p>
<?php if ( ! $this->is_activitypub_active() ) : ?>
<p class="description" style="color: #dba617;">
<span class="dashicons dashicons-warning" style="font-size: 16px; width: 16px; height: 16px; vertical-align: text-bottom;"></span>
<?php esc_html_e( 'The ActivityPub plugin is recommended for full Fediverse integration.', 'wp-fedistream' ); ?>
</p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'WooCommerce Integration', 'wp-fedistream' ); ?></th>
<td>
<label>
<input type="checkbox" name="enable_woocommerce" value="1" <?php checked( $enable_woocommerce, 1 ); ?> <?php disabled( ! $this->is_woocommerce_active() ); ?>>
<?php esc_html_e( 'Enable WooCommerce features', 'wp-fedistream' ); ?>
</label>
<?php if ( ! $this->is_woocommerce_active() ) : ?>
<p class="description" style="color: #d63638;">
<span class="dashicons dashicons-dismiss" style="font-size: 16px; width: 16px; height: 16px; vertical-align: text-bottom;"></span>
<?php esc_html_e( 'WooCommerce is not installed or active.', 'wp-fedistream' ); ?>
</p>
<?php else : ?>
<p class="description"><?php esc_html_e( 'Sell albums and tracks through WooCommerce.', 'wp-fedistream' ); ?></p>
<?php endif; ?>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<?php <?php
} }

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP FediStream * Plugin Name: WP FediStream
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-fedistream * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-fedistream
* Description: Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels. * Description: Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels.
* Version: 0.2.0 * Version: 0.3.0
* Requires at least: 6.4 * Requires at least: 6.4
* Requires PHP: 8.3 * Requires PHP: 8.3
* Author: Marco Graetsch * Author: Marco Graetsch
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
* *
* @var string * @var string
*/ */
define( 'WP_FEDISTREAM_VERSION', '0.2.0' ); define( 'WP_FEDISTREAM_VERSION', '0.3.0' );
/** /**
* Plugin file path. * Plugin file path.