24 Commits

Author SHA1 Message Date
fedab21c2a fix: Complete memory leak fix with shortcode context tracking
All checks were successful
Create Release Package / build-release (push) Successful in 57s
The v0.4.1 fix was incomplete - shortcodes called get_*_data() methods
directly, bypassing the recursion tracking in get_post_data().

Changes:
- Added $in_shortcode_context flag to TemplateLoader
- Added enter/exit_shortcode_context() methods
- All shortcode render methods now enter context before data loading
- When in shortcode context, the_content filter is always skipped

This fully prevents infinite recursion when post content contains
FediStream shortcodes that would otherwise recursively render.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:44:51 +01:00
eaefcff9c9 fix: Critical memory leak in TemplateLoader causing OOM errors
All checks were successful
Create Release Package / build-release (push) Successful in 57s
- Added recursion depth tracking to prevent infinite loops from shortcodes in content
- Nested items now skip the_content filter, using wp_kses_post() instead
- Made get_artist_data(), get_album_data(), get_track_data(), get_playlist_data() public
- Methods now accept both int post IDs and WP_Post objects
- Added $load_nested parameter to control nested item loading

Fixes memory exhaustion in Twig's StagingExtension when post content
contains FediStream shortcodes that trigger recursive template rendering.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:37:43 +01:00
04201a66f8 docs: Update session history with CI/CD fixes
Document all CI/CD pipeline fixes:
- Gitea API for releases (not GitHub action)
- Git submodule with relative URL
- Composer path repository
- gettext installation
- SIGPIPE fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:46:06 +01:00
8ae703787c chore: Reorder exclusions in release workflow
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:38:37 +01:00
c540cde0a4 docs: Update README for v0.4.0
- Update version badge to 0.4.0
- Add CI/CD badge
- Add release package installation instructions
- Add license key section
- Add releases section
- Update from-source instructions for submodules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:35:06 +01:00
d96e3e3a4d chore: Exclude .gitea and .gitmodules from release package
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:33:54 +01:00
20c879c065 fix: Handle SIGPIPE in package verification
All checks were successful
Create Release Package / build-release (push) Successful in 58s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:27:22 +01:00
d104b0ae46 fix: Install gettext for msgfmt in CI
Some checks failed
Create Release Package / build-release (push) Failing after 56s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:24:04 +01:00
98ddb63d44 fix: Use relative URL for submodule
Some checks failed
Create Release Package / build-release (push) Failing after 50s
Relative URL allows CI to use the same access method as the main repo,
avoiding networking issues with absolute URLs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:21:21 +01:00
3dd2f4d126 feat: Use git submodule for private dependency
Some checks failed
Create Release Package / build-release (push) Has been cancelled
- Add wc-licensed-product-client as git submodule in lib/
- Change Composer repository from VCS to path type
- CI now checks out submodules recursively
- Remove COMPOSER_AUTH (no longer needed)

This solves the CI networking issue by fetching the
dependency via git submodule instead of Composer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 18:18:14 +01:00
dfa405c89b fix: Add Composer auth for private repository in CI
Some checks failed
Create Release Package / build-release (push) Has been cancelled
Use COMPOSER_AUTH with HTTP basic auth to access private
wc-licensed-product-client repository during builds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:11:05 +01:00
a7dbb7b4c5 fix: Use SRC_GITEA_TOKEN secret for releases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:01:55 +01:00
efd3f7a170 chore: Update license client to ^0.2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:47:32 +01:00
077093765e chore: Update license client to dev-main
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:44:04 +01:00
d4425103ea fix: Use Gitea API directly for release creation
Some checks failed
Create Release Package / build-release (push) Failing after 3m6s
The actions/gitea-release-action doesn't exist on GitHub.
Use curl with Gitea API instead for reliable release creation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:31:53 +01:00
02e2ed82ef docs: Update session history for v0.4.0 CI/CD pipeline
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 15:43:34 +01:00
b21394c674 feat: Add Gitea Actions CI/CD pipeline for releases (v0.4.0)
Some checks failed
Create Release Package / build-release (push) Failing after 4s
- Automated release builds triggered by v* tags
- PHP 8.3 environment with production dependencies
- Automatic translation compilation (.po to .mo)
- Version verification (plugin version must match tag)
- WordPress-compliant zip structure
- SHA256 checksum generation
- Package structure verification
- Changelog extraction for release notes
- Automatic Gitea release creation with attachments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 15:40:08 +01:00
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
db67a17fd4 feat: Add plugin action links and user guide
- Add Settings and Dashboard links to the WordPress Plugins page
- Create comprehensive user guide (USERGUIDE.md) covering:
  - Installation and configuration
  - Managing artists, albums, tracks, and playlists
  - Shortcodes and widgets
  - ActivityPub and WooCommerce integration
  - User library features and troubleshooting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:52:12 +01:00
96269eb355 fix: WooCommerce product types not appearing in selector
Fixed timing issue where WooCommerce integration hooks were registered
too late during plugins_loaded. The constructor now calls check_woocommerce()
directly instead of hooking it at priority 5 (which had already passed).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:47:56 +01:00
01c256349f docs: Update session history with successful push
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:28:44 +01:00
38377f11e0 docs: Update CLAUDE.md with session history and cleanup roadmap
- Added session history entry for v0.1.0 release
- Cleaned up temporary roadmap section
- Created empty version sections for 0.1.1 and 0.2.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 23:24:52 +01:00
22 changed files with 7827 additions and 175 deletions

View File

@@ -0,0 +1,195 @@
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 --strict
- name: Install Composer dependencies (production)
run: |
composer config platform.php 8.3.0
composer install --no-dev --optimize-autoloader --no-interaction
- 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]+" wp-fedistream.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="wp-fedistream"
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
# Move to parent directory for proper zip structure
cd ..
# Create zip with proper WordPress plugin structure
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}/.gitea/*" \
-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 "*.DS_Store"
cd "${PLUGIN_NAME}"
echo "Created: ${RELEASE_FILE}"
- name: Generate checksums
run: |
VERSION=${{ steps.version.outputs.version }}
RELEASE_FILE="releases/wp-fedistream-${VERSION}.zip"
cd releases
sha256sum "wp-fedistream-${VERSION}.zip" > "wp-fedistream-${VERSION}.zip.sha256"
echo "SHA256:"
cat "wp-fedistream-${VERSION}.zip.sha256"
- name: Verify package structure
run: |
set +o pipefail
VERSION=${{ steps.version.outputs.version }}
echo "Package contents:"
unzip -l "releases/wp-fedistream-${VERSION}.zip" | head -50 || true
# Verify main file is at correct location
if unzip -l "releases/wp-fedistream-${VERSION}.zip" | grep -q "wp-fedistream/wp-fedistream.php"; then
echo "✓ Main plugin file at correct location"
else
echo "✗ Error: Main plugin file not found at wp-fedistream/wp-fedistream.php"
exit 1
fi
# Verify vendor directory is included
if unzip -l "releases/wp-fedistream-${VERSION}.zip" | grep -q "wp-fedistream/vendor/"; then
echo "✓ Vendor directory included"
else
echo "✗ Error: Vendor directory not found"
exit 1
fi
- name: Extract changelog for release notes
id: changelog
run: |
VERSION=${{ steps.version.outputs.version }}
# Extract changelog section for this version
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
if [ -z "$NOTES" ]; then
NOTES="Release version ${VERSION}"
fi
# Save to file for multi-line output
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 }}
PRERELEASE="false"
if [[ "$TAG_NAME" == *-* ]]; then
PRERELEASE="true"
fi
# Read release notes
BODY=$(cat release_notes.txt)
# 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 attachments
for file in "releases/wp-fedistream-${VERSION}.zip" "releases/wp-fedistream-${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 successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "lib/wc-licensed-product-client"]
path = lib/wc-licensed-product-client
url = ../wc-licensed-product-client.git

View File

@@ -7,6 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.4.2] - 2026-02-02
### Fixed
- **Complete fix for memory leak** - v0.4.1 fix was incomplete
- Added `$in_shortcode_context` flag to TemplateLoader to track when we're rendering shortcodes
- All shortcode render methods now call `enter_shortcode_context()` before loading data
- When in shortcode context, `the_content` filter is always skipped to prevent recursive shortcode processing
- This prevents infinite recursion when post content contains FediStream shortcodes
## [0.4.1] - 2026-02-02
### Fixed
- **Critical memory leak** causing "Allowed memory size exhausted" errors in Twig's StagingExtension
- Root cause: `apply_filters('the_content')` in `get_post_data()` triggered shortcode processing, causing infinite recursion when post content contained FediStream shortcodes
- Added recursion depth tracking with `MAX_RECURSION_DEPTH = 3` to prevent runaway nesting
- Nested items now skip `the_content` filter, using `wp_kses_post()` instead
- Nested data loading (albums within artists, tracks within albums) is now properly bounded
### Changed
- Made `get_artist_data()`, `get_album_data()`, `get_track_data()`, and `get_playlist_data()` public methods in TemplateLoader (previously private but called externally)
- These methods now accept both `int` post IDs and `WP_Post` objects for flexibility
- Added `$load_nested` parameter to control whether nested items are fully loaded or just counted
## [0.4.0] - 2026-01-29
### Added
- Gitea Actions CI/CD pipeline for automated release package creation
- Triggered by `v*` tags
- PHP 8.3 environment with production dependencies
- Automatic translation compilation (.po to .mo)
- Version verification (plugin version must match tag)
- WordPress-compliant zip structure
- SHA256 checksum generation
- Package structure verification
- Changelog extraction for release notes
- Automatic Gitea release creation with attachments
- Pre-release detection for tags containing `-`
## [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
### Added
- Settings and Dashboard links on the WordPress Plugins page for quick access
- Comprehensive user guide (`USERGUIDE.md`) covering installation, configuration, and all features
## [0.1.1] - 2026-01-28
### Fixed
- WooCommerce product types (FediStream Album/Track) not appearing in product type selector
- Fixed timing issue where WooCommerce integration hooks were registered too late during `plugins_loaded`
## [0.1.0] - 2026-01-28
Initial release of WP FediStream - a WordPress plugin for streaming music over ActivityPub.
@@ -139,5 +217,11 @@ Initial release of WP FediStream - a WordPress plugin for streaming music over A
---
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.1.0...HEAD
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.4.2...HEAD
[0.4.2]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.4.1...v0.4.2
[0.4.1]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.4.0...v0.4.1
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.3.0...v0.4.0
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.2.0...v0.3.0
[0.2.0]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.1.1...v0.2.0
[0.1.1]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.1.0...v0.1.1
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/releases/tag/v0.1.0

217
CLAUDE.md
View File

@@ -24,14 +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.
### Version 0.1.0
- Document all relevant implementation details in CLAUDE.md, drop PLAN.md, because we don't need it anymore. All relevent Infos are kept in CLAUDE.md from now on
- Drop the current versioning, as this will be version 0.1.0. Merge the history contents in CHANGELOG.md into as single version 0.1.0
- Update the README.md according to the last changes and current implementation
- Commit the current sources to dev, merge it to main, tag it as 0.1.0 and push it all to origin
- Cleanup this Version entry from the temporary raodmap and create to empty sections for the next bugfix-version and the minor version.
- Call `/end-session`
(No pending features - all roadmap items completed)
## Technical Stack
@@ -133,11 +126,48 @@ for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
#### What's Excluded
- Git metadata (`.git/`)
- Development files (`.vscode/`, `.claude/`, `CLAUDE.md`, `wp-core`, `wp-plugins`)
- Development files (`.vscode/`, `.claude/`, `.gitea/`, `CLAUDE.md`, `wp-core`, `wp-plugins`)
- Logs and cache files
- Previous releases
- `composer.lock` (but `vendor/` is included)
#### CI/CD Pipeline (Gitea Actions)
Automated release packages are created via Gitea Actions when a tag matching `v*` is pushed:
**Workflow:** `.gitea/workflows/release.yml`
**Trigger:** Push tag `vX.X.X` to repository
**Steps:**
1. Checkout code
2. Setup PHP 8.3 with required extensions
3. Install production Composer dependencies
4. Compile translations (.po to .mo)
5. Verify plugin version matches tag version
6. Build release zip with proper WordPress structure
7. Generate SHA256 checksums
8. Verify package structure
9. Extract changelog for release notes
10. Create Gitea release with attachments
**Required Secret:** `GITEA_TOKEN` - Personal access token with release permissions
**Pre-release Detection:** Tags containing `-` (e.g., `v1.0.0-beta`) are marked as pre-release
**To create a release:**
```bash
# Ensure version is updated in wp-fedistream.php (both header and constant)
git checkout main
git merge dev
git tag -a v0.4.0 -m "Release v0.4.0"
git push origin main --tags
```
The pipeline will automatically build and publish the release package.
---
**For AI Assistants:**
@@ -191,6 +221,9 @@ When editing CLAUDE.md or other markdown files, follow these rules to avoid lint
```txt
wp-fedistream/
├── .gitea/
│ └── workflows/
│ └── release.yml # CI/CD release pipeline
├── assets/
│ ├── css/
│ │ ├── admin.css # Admin interface styles
@@ -353,3 +386,169 @@ wp-fedistream/
---
## Session History
### 2026-01-28 - Initial Release v0.1.0
**Summary:** Consolidated all development phases (0.0.1 through 0.7.0) into initial release v0.1.0.
**Completed:**
- Implemented Phase 6 (WooCommerce Integration):
- Custom product types for albums and tracks
- Pricing models (Fixed, PWYW, NYP)
- Digital delivery with secure downloads
- Streaming access control based on purchases
- Implemented Phase 7 (User Interactions):
- User library with favorites, follows, history
- Notification system (in-app and email)
- Library shortcode and frontend page
- Consolidated documentation:
- Moved implementation details from PLAN.md to CLAUDE.md
- Deleted PLAN.md (no longer needed)
- Merged all changelog entries into single v0.1.0 release
- Updated README.md with current features
- Git operations:
- Created initial commit on dev branch
- Merged to main branch
- Tagged as v0.1.0
- Push pending (requires credentials)
**Files Created:**
- `includes/WooCommerce/Integration.php`
- `includes/WooCommerce/AlbumProduct.php`
- `includes/WooCommerce/TrackProduct.php`
- `includes/WooCommerce/DigitalDelivery.php`
- `includes/WooCommerce/StreamingAccess.php`
- `includes/User/Library.php`
- `includes/User/LibraryPage.php`
- `includes/User/Notifications.php`
- `assets/js/library.js`
- `assets/js/notifications.js`
**Files Deleted:**
- `PLAN.md`
**Notes:**
- Successfully pushed dev, main branches and v0.1.0 tag to origin
- Remote URL updated from HTTPS to SSH for authentication
- 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
### 2026-01-29 - CI/CD Pipeline v0.4.0
**Summary:** Added Gitea Actions workflow for automated release package creation with multiple iterations to fix CI issues.
**Features:**
- Automated release builds triggered by `v*` tags
- PHP 8.3 environment with required extensions
- Production Composer dependency installation
- Automatic translation compilation (.po to .mo)
- Version verification (plugin version must match tag)
- Proper WordPress plugin zip structure
- SHA256 checksum generation
- Package structure verification
- Changelog extraction for release notes
- Automatic Gitea release creation via API
- Pre-release detection for tags containing `-`
**Files Created:**
- `.gitea/workflows/release.yml` - CI/CD release pipeline
- `.gitmodules` - Git submodule configuration
- `lib/wc-licensed-product-client/` - Submodule for private dependency
**Files Modified:**
- `CLAUDE.md` - Added CI/CD documentation and updated directory structure
- `CHANGELOG.md` - Added v0.4.0 entry
- `wp-fedistream.php` - Version bump to 0.4.0
- `composer.json` - Changed to path repository for submodule
- `README.md` - Updated for v0.4.0, added release/installation docs
**CI/CD Fixes Applied:**
1. `actions/gitea-release-action@v1` doesn't exist - use Gitea API directly with curl
2. Private repo network issue - use git submodule with relative URL (`../wc-licensed-product-client.git`)
3. Composer path repository for submodule dependency
4. `msgfmt` not found - install gettext package
5. SIGPIPE error (exit 141) - use `set +o pipefail` and `|| true`
**Notes:**
- Requires `SRC_GITEA_TOKEN` secret configured in repository settings
- Uses `shivammathur/setup-php@v2` for PHP setup
- Uses Gitea API directly for release creation (not GitHub Actions)
- Submodule uses relative URL for CI compatibility
- Composer symlinks from `lib/wc-licensed-product-client` to vendor

View File

@@ -2,10 +2,11 @@
Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels.
[![Version](https://img.shields.io/badge/version-0.1.0-blue.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.4.0-blue.svg)](CHANGELOG.md)
[![PHP](https://img.shields.io/badge/PHP-%3E%3D8.3-purple.svg)](https://php.net)
[![WordPress](https://img.shields.io/badge/WordPress-%3E%3D6.4-blue.svg)](https://wordpress.org)
[![License](https://img.shields.io/badge/license-GPL--2.0%2B-green.svg)](https://www.gnu.org/licenses/gpl-2.0.html)
[![CI/CD](https://img.shields.io/badge/CI%2FCD-Gitea%20Actions-green.svg)](https://src.bundespruefstelle.ch/magdev/wp-fedistream/actions)
## Description
@@ -31,29 +32,47 @@ WP FediStream is a WordPress plugin that enables musicians, bands, and labels to
- PHP 8.3 or higher
- WordPress 6.4 or higher
- Composer (for development/installation)
- Valid license key (required for frontend features)
### Optional
- [ActivityPub Plugin](https://wordpress.org/plugins/activitypub/) - For Fediverse integration
- [WooCommerce](https://woocommerce.com/) 10.0+ - For selling music
## License Key
WP FediStream requires a valid license key for frontend functionality (player, shortcodes, ActivityPub). The admin dashboard works without a license, allowing you to configure the plugin before activation.
To obtain a license key, contact the author or purchase from the official website.
## Installation
### From Source
### From Release Package (Recommended)
1. Clone or download the repository to your WordPress plugins directory:
1. Download the latest release from the [Releases page](https://src.bundespruefstelle.ch/magdev/wp-fedistream/releases)
2. Upload the ZIP file via **Plugins > Add New > Upload Plugin** in WordPress admin
3. Activate the plugin under **Plugins > Installed Plugins**
4. Navigate to **FediStream > Settings** and enter your license key
5. Start using the plugin via the **FediStream** admin menu
### From Source (Development)
1. Clone the repository to your WordPress plugins directory:
```bash
cd wp-content/plugins/
git clone https://src.bundespruefstelle.ch/magdev/wp-fedistream.git
git clone --recurse-submodules https://src.bundespruefstelle.ch/magdev/wp-fedistream.git
```
2. Install Composer dependencies:
```bash
cd wp-fedistream
composer install --no-dev
composer install
```
3. Activate the plugin in WordPress admin under **Plugins > Installed Plugins**
@@ -133,6 +152,16 @@ wp-fedistream/
└── wp-fedistream.php # Plugin entry point
```
## Releases
Release packages are automatically built via Gitea Actions when a version tag is pushed. Each release includes:
- Production-ready ZIP package with all dependencies
- SHA256 checksum for verification
- Changelog notes extracted from CHANGELOG.md
Download releases from: <https://src.bundespruefstelle.ch/magdev/wp-fedistream/releases>
## Contributing
This project is in early development. Contributions, bug reports, and feature requests are welcome.

550
USERGUIDE.md Normal file
View File

@@ -0,0 +1,550 @@
# WP FediStream User Guide
A comprehensive guide to setting up and using WP FediStream, the WordPress plugin for streaming music over ActivityPub.
## Table of Contents
1. [Installation](#installation)
2. [Initial Configuration](#initial-configuration)
3. [Managing Artists](#managing-artists)
4. [Creating Albums](#creating-albums)
5. [Adding Tracks](#adding-tracks)
6. [Building Playlists](#building-playlists)
7. [Using Shortcodes](#using-shortcodes)
8. [Widgets](#widgets)
9. [ActivityPub Integration](#activitypub-integration)
10. [WooCommerce Integration](#woocommerce-integration)
11. [User Library Features](#user-library-features)
12. [Troubleshooting](#troubleshooting)
---
## Installation
### Requirements
- PHP 8.3 or higher
- WordPress 6.4 or higher
- Composer (for installation from source)
### Optional Requirements
- [ActivityPub Plugin](https://wordpress.org/plugins/activitypub/) - For Fediverse integration
- [WooCommerce](https://woocommerce.com/) 10.0+ - For selling music
### From Source
1. Navigate to your WordPress plugins directory:
```bash
cd wp-content/plugins/
```
2. Clone or download the repository:
```bash
git clone https://src.bundespruefstelle.ch/magdev/wp-fedistream.git
```
3. Install Composer dependencies:
```bash
cd wp-fedistream
composer install --no-dev
```
4. Activate the plugin in WordPress admin under **Plugins > Installed Plugins**
5. Navigate to **FediStream** in the admin menu to get started
---
## Initial Configuration
After activation, configure FediStream through **FediStream > Settings**.
### General Settings
| Setting | Description | Default |
| --------- | ------------- | --------- |
| ActivityPub Integration | Enable Fediverse features for sharing releases | Enabled |
| WooCommerce Integration | Enable selling music (requires WooCommerce) | Disabled |
| Max Upload Size | Maximum audio file size in MB | 50 MB |
| Default License | Default copyright license for new uploads | All Rights Reserved |
### License Options
FediStream supports various licensing options:
- **All Rights Reserved** - Traditional copyright
- **CC BY** - Creative Commons Attribution
- **CC BY-SA** - Attribution-ShareAlike
- **CC BY-NC** - Attribution-NonCommercial
- **CC BY-NC-SA** - Attribution-NonCommercial-ShareAlike
- **CC0** - Public Domain Dedication
---
## Managing Artists
Artists represent musicians, bands, duos, or collectives. Each artist can have their own ActivityPub presence.
### Creating an Artist
1. Go to **FediStream > Artists > Add New**
2. Fill in the required information:
- **Name** - Artist or band name
- **Biography** - Detailed description (supports rich text)
- **Featured Image** - Artist photo/logo
3. Configure artist details in the meta boxes:
- **Artist Type** - Solo, Band, Duo, or Collective
- **Formation Date** - When the artist/band started
- **Location** - City, Country
- **Website** - Official website URL
4. Add social media links:
- Mastodon, Twitter/X, Instagram, Facebook, YouTube, Spotify, Bandcamp, SoundCloud
5. For bands: Add band members with their names and roles
6. Assign genres from the Genre taxonomy
7. Publish the artist
### Artist Types
| Type | Description |
| ------ | ------------- |
| Solo | Individual musician |
| Band | Group of musicians |
| Duo | Two-person musical act |
| Collective | Loose group of collaborating artists |
---
## Creating Albums
Albums organize your music into releases. They can be full albums, EPs, singles, or compilations.
### Creating an Album
1. Go to **FediStream > Albums > Add New**
2. Enter the album information:
- **Title** - Album name
- **Description** - Album notes, liner notes, story
- **Featured Image** - Album artwork (recommended: 1400x1400 pixels)
3. Configure album metadata:
- **Artist** - Select the primary artist
- **Album Type** - Album, EP, Single, or Compilation
- **Release Date** - Official release date
- **Catalog Number** - Your catalog reference (optional)
- **UPC/EAN** - Universal Product Code (optional)
4. Assign genres and license
5. Publish the album
### Album Types
| Type | Description | Typical Track Count |
| ------ | ------------- | --------------------- |
| Album | Full-length release | 8-15 tracks |
| EP | Extended Play | 4-6 tracks |
| Single | Single track release | 1-3 tracks |
| Compilation | Collection of tracks | Varies |
---
## Adding Tracks
Tracks are the individual songs or audio files in your library.
### Creating a Track
1. Go to **FediStream > Tracks > Add New**
2. Enter track information:
- **Title** - Song title
- **Description** - Lyrics, notes, credits
3. Upload the audio file:
- Click **Select Audio File** in the Track Audio meta box
- Supported formats: MP3, WAV, FLAC, OGG, AAC
- Recommended: High-quality MP3 (320kbps) or FLAC
4. Configure track metadata:
- **Album** - Associate with an album
- **Track Number** - Position in the album
- **Disc Number** - For multi-disc releases
- **Duration** - Auto-detected from audio file
- **BPM** - Beats per minute (optional)
- **Key** - Musical key (optional)
- **ISRC** - International Standard Recording Code (optional)
5. Set the featured image (track artwork, optional)
6. Assign genres, moods, and license
7. Publish the track
### Audio File Guidelines
| Format | Quality | Recommended For |
| -------- | --------- | ----------------- |
| MP3 | 320kbps | Web streaming |
| FLAC | Lossless | Downloads, archival |
| WAV | Uncompressed | Master files |
| OGG | Variable | Alternative streaming |
---
## Building Playlists
Playlists are curated collections of tracks, useful for themed collections or user-generated content.
### Creating a Playlist
1. Go to **FediStream > Playlists > Add New**
2. Enter playlist information:
- **Title** - Playlist name
- **Description** - What the playlist is about
- **Featured Image** - Playlist cover art
3. Add tracks to the playlist:
- Use the **Playlist Tracks** meta box
- Search for tracks by title
- Click to add tracks to the playlist
- Drag and drop to reorder tracks
4. Configure visibility:
- **Public** - Visible to everyone
- **Private** - Only visible to the creator
5. Assign moods (optional)
6. Publish the playlist
---
## Using Shortcodes
FediStream provides shortcodes to display content anywhere in your WordPress site.
### Available Shortcodes
#### Display Single Artist
```txt
[fedistream_artist id="123"]
```
Shows the artist profile with bio, image, and social links.
**Attributes:**
- `id` (required) - Artist post ID
#### Display Album
```txt
[fedistream_album id="456"]
```
Shows the album with artwork, tracklist, and play buttons.
**Attributes:**
- `id` (required) - Album post ID
- `show_tracks` - Show tracklist (default: true)
#### Display Track
```txt
[fedistream_track id="789"]
```
Shows a single track with player controls.
**Attributes:**
- `id` (required) - Track post ID
#### Display Playlist
```txt
[fedistream_playlist id="101"]
```
Shows the playlist with all tracks and player.
**Attributes:**
- `id` (required) - Playlist post ID
#### Latest Releases
```txt
[fedistream_latest_releases count="5"]
```
Shows a grid of recent album releases.
**Attributes:**
- `count` - Number of releases to show (default: 6)
- `columns` - Grid columns (default: 3)
#### Popular Tracks
```txt
[fedistream_popular_tracks count="10"]
```
Shows a list of most-played tracks.
**Attributes:**
- `count` - Number of tracks to show (default: 10)
#### Artists Grid
```txt
[fedistream_artists count="12" columns="4"]
```
Shows a grid of artists.
**Attributes:**
- `count` - Number of artists (default: 12)
- `columns` - Grid columns (default: 4)
#### Audio Player Widget
```txt
[fedistream_player]
```
Shows the persistent audio player.
#### User Library
```txt
[fedistream_library]
```
Shows the logged-in user's music library (favorites, followed artists, history).
---
## Widgets
FediStream includes widgets for your sidebar or widget areas.
### Available Widgets
| Widget | Description |
| -------- | ------------- |
| Recent Releases | Displays latest album releases |
| Popular Tracks | Shows most-played tracks |
| Featured Artist | Highlights a specific artist |
| Now Playing | Shows currently playing track |
### Adding Widgets
1. Go to **Appearance > Widgets**
2. Find FediStream widgets in the available widgets list
3. Drag to your desired widget area
4. Configure the widget options
5. Save
---
## ActivityPub Integration
FediStream integrates with the Fediverse through ActivityPub, allowing your artists to be followed from Mastodon, Pixelfed, and other platforms.
### Requirements for ActivityPub integration
Install and activate the [ActivityPub plugin](https://wordpress.org/plugins/activitypub/) from WordPress.org.
### How It Works
1. Each artist becomes an ActivityPub actor (like a Mastodon account)
2. Fediverse users can follow artists using their handle: `@artist-slug@your-domain.com`
3. When you publish new albums or tracks, announcements are sent to followers
4. Followers can like and boost your releases, which shows on your site
### Artist Discovery (Webfinger)
Artists can be discovered via Webfinger:
```txt
https://your-site.com/.well-known/webfinger?resource=acct:artist-name@your-site.com
```
### Activity Types
| Activity | When Sent |
| ---------- | ----------- |
| Create | New album or track published |
| Update | Album or track updated |
| Delete | Content removed |
| Follow/Accept | New follower confirmed |
### Viewing Followers
Artist followers are displayed on the artist's admin page under the Followers meta box.
---
## WooCommerce Integration
Sell your music directly through your WordPress site using WooCommerce.
### Requirements for WooCommerce Integration
- WooCommerce 10.0 or higher installed and activated
- WooCommerce integration enabled in FediStream settings
### Setting Up Products
1. Enable WooCommerce in **FediStream > Settings**
2. Go to **Products > Add New**
3. Select product type:
- **FediStream Album** - Sell a complete album
- **FediStream Track** - Sell individual tracks
4. Configure the FediStream tab:
- Link to the corresponding album or track
- Set pricing type (Fixed, Pay What You Want, Name Your Price)
- Configure minimum and suggested prices for PWYW
- Select available download formats
- Enable streaming access for purchasers
5. Publish the product
### Pricing Types
| Type | Description |
| ------ | ------------- |
| Fixed | Standard fixed price |
| Pay What You Want (PWYW) | Customer chooses price above minimum |
| Name Your Price (NYP) | Customer sets any price (including free) |
### Download Formats
Customers can download purchased music in these formats:
- MP3 (320kbps)
- FLAC (Lossless)
- WAV (Uncompressed)
- AAC (256kbps)
- OGG Vorbis
### Streaming Access
When "Include Streaming" is enabled:
- Non-purchasers hear 30-second previews
- Purchasers get full-quality streaming access
- Access is tied to the customer account
---
## User Library Features
Logged-in users can build their personal music library.
### Favorites
- Click the heart icon on any track, album, or playlist to save it
- View all favorites in the Library page
### Following Artists
- Click "Follow" on any artist profile
- Get notified when they release new music
### Listening History
- Automatically tracks what you've listened to
- View recent plays in the Library page
- Option to clear history
### Notifications
Users receive notifications for:
- New releases from followed artists
- New followers (for artists)
- Fediverse interactions (likes, boosts)
- Purchases (for WooCommerce)
Notifications appear in the admin bar and can be configured for email delivery.
---
## Troubleshooting
### Audio Files Not Playing
1. Verify the audio file is properly uploaded
2. Check browser console for errors
3. Ensure the audio format is supported
4. Verify file permissions on the server
### ActivityPub Not Working
1. Ensure the ActivityPub plugin is installed and active
2. Check that ActivityPub is enabled in FediStream settings
3. Verify your site's SSL certificate is valid
4. Check that `.well-known/webfinger` is accessible
### WooCommerce Products Not Showing
1. Ensure WooCommerce is installed and activated
2. Enable WooCommerce in FediStream settings
3. Clear any caching plugins
4. Check for JavaScript errors in browser console
### Images Not Displaying
1. Check that featured images are set on posts
2. Verify the Media Library has the images
3. Check theme compatibility with featured images
4. Regenerate thumbnails if image sizes are wrong
### Performance Issues
1. Consider using a caching plugin
2. Optimize images before upload
3. Use a CDN for audio files
4. Enable object caching if available
### Getting Help
- **Documentation**: Check this guide and the README
- **Issues**: Report bugs at the [issue tracker](https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues)
- **Updates**: Keep the plugin updated for latest fixes
---
## Quick Reference
### URL Slugs
| Content Type | URL Pattern |
| -------------- | ------------- |
| Artists | `/artists/{slug}/` |
| Albums | `/albums/{slug}/` |
| Tracks | `/tracks/{slug}/` |
| Playlists | `/playlists/{slug}/` |
| Genres | `/genre/{slug}/` |
| Moods | `/mood/{slug}/` |
### User Roles
| Role | Capabilities |
| ------ | -------------- |
| FediStream Artist | Manage own content, upload files, view own stats |
| FediStream Label | Manage all content, manage taxonomies, view all stats |
### Keyboard Shortcuts (Player)
| Key | Action |
| ----- | -------- |
| Space | Play/Pause |
| Left Arrow | Previous track |
| Right Arrow | Next track |
| Up Arrow | Volume up |
| Down Arrow | Volume down |
| M | Mute/Unmute |
| S | Shuffle toggle |
| R | Repeat mode cycle |
---
*For more information, visit the [project repository](https://src.bundespruefstelle.ch/magdev/wp-fedistream).*

View File

@@ -4,4 +4,122 @@
* @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';
$(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);

View File

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

654
composer.lock generated
View File

@@ -4,8 +4,314 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c8fb50541e5730c8ad92b76392765aca",
"content-hash": "0c8153ac31232ffe7f0af117cec865b4",
"packages": [
{
"name": "magdev/wc-licensed-product-client",
"version": "v0.2.2",
"dist": {
"type": "path",
"url": "lib/wc-licensed-product-client",
"reference": "f9281ec5fb23bf1993ab0240e0347c835009a10f"
},
"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"
},
"transport-options": {
"relative": true
}
},
{
"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",
"version": "v3.6.0",
@@ -73,6 +379,185 @@
],
"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",
"version": "v1.33.0",
@@ -241,6 +726,173 @@
],
"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",
"version": "v3.23.0",

View File

@@ -7,6 +7,8 @@
namespace WP_FediStream\Frontend;
use WP_FediStream\License\Manager as LicenseManager;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -36,6 +38,11 @@ class Ajax {
* @return 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.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
@@ -125,6 +132,11 @@ class Ajax {
* @return 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.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );

View File

@@ -27,13 +27,35 @@ class Shortcodes {
private Plugin $plugin;
/**
* Constructor.
* Whether running in unlicensed mode.
*
* @var bool
*/
public function __construct() {
$this->plugin = Plugin::get_instance();
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->unlicensed_mode = $unlicensed_mode;
$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.
*
@@ -59,6 +81,9 @@ class Shortcodes {
* @return string
*/
public function render_artist( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'id' => 0,
@@ -73,6 +98,7 @@ class Shortcodes {
$post = $this->get_post( $atts, 'fedistream_artist' );
if ( ! $post ) {
TemplateLoader::exit_shortcode_context();
return '';
}
@@ -97,6 +123,9 @@ class Shortcodes {
* @return string
*/
public function render_album( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'id' => 0,
@@ -110,6 +139,7 @@ class Shortcodes {
$post = $this->get_post( $atts, 'fedistream_album' );
if ( ! $post ) {
TemplateLoader::exit_shortcode_context();
return '';
}
@@ -133,6 +163,9 @@ class Shortcodes {
* @return string
*/
public function render_track( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'id' => 0,
@@ -146,6 +179,7 @@ class Shortcodes {
$post = $this->get_post( $atts, 'fedistream_track' );
if ( ! $post ) {
TemplateLoader::exit_shortcode_context();
return '';
}
@@ -169,6 +203,9 @@ class Shortcodes {
* @return string
*/
public function render_playlist( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'id' => 0,
@@ -182,6 +219,7 @@ class Shortcodes {
$post = $this->get_post( $atts, 'fedistream_playlist' );
if ( ! $post ) {
TemplateLoader::exit_shortcode_context();
return '';
}
@@ -205,6 +243,9 @@ class Shortcodes {
* @return string
*/
public function render_latest_releases( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'count' => 6,
@@ -270,6 +311,9 @@ class Shortcodes {
* @return string
*/
public function render_popular_tracks( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'count' => 10,
@@ -337,6 +381,9 @@ class Shortcodes {
* @return string
*/
public function render_artists_grid( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'count' => 12,
@@ -404,6 +451,9 @@ class Shortcodes {
* @return string
*/
public function render_player( array $atts ): string {
// Enter shortcode context to prevent recursive shortcode processing during data loading.
TemplateLoader::enter_shortcode_context();
$atts = shortcode_atts(
array(
'track' => 0,
@@ -449,6 +499,7 @@ class Shortcodes {
}
if ( empty( $tracks ) ) {
TemplateLoader::exit_shortcode_context();
return '';
}
@@ -501,13 +552,25 @@ class Shortcodes {
* @return string
*/
private function render_template( string $template, array $context ): string {
// Check for unlicensed mode.
if ( $this->unlicensed_mode ) {
return $this->get_unlicensed_message();
}
// Enter shortcode context to prevent recursive shortcode processing.
TemplateLoader::enter_shortcode_context();
try {
return $this->plugin->render( $template, $context );
$result = $this->plugin->render( $template, $context );
} catch ( \Exception $e ) {
TemplateLoader::exit_shortcode_context();
if ( WP_DEBUG ) {
return '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
}
return '';
}
TemplateLoader::exit_shortcode_context();
return $result;
}
}

View File

@@ -21,6 +21,57 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class TemplateLoader {
/**
* Recursion depth for get_post_data calls.
*
* @var int
*/
private static int $recursion_depth = 0;
/**
* Maximum allowed recursion depth.
*
* @var int
*/
private const MAX_RECURSION_DEPTH = 3;
/**
* Whether we're currently in a shortcode rendering context.
* When true, the_content filter is skipped to prevent recursive shortcode processing.
*
* @var bool
*/
private static bool $in_shortcode_context = false;
/**
* Enter shortcode rendering context.
* Call this before rendering shortcode content to prevent recursive shortcode processing.
*
* @return void
*/
public static function enter_shortcode_context(): void {
self::$in_shortcode_context = true;
}
/**
* Exit shortcode rendering context.
* Call this after shortcode rendering is complete.
*
* @return void
*/
public static function exit_shortcode_context(): void {
self::$in_shortcode_context = false;
}
/**
* Check if we're in a shortcode rendering context.
*
* @return bool
*/
public static function is_in_shortcode_context(): bool {
return self::$in_shortcode_context;
}
/**
* Constructor.
*/
@@ -191,14 +242,23 @@ class TemplateLoader {
/**
* Get post data for template.
*
* @param \WP_Post $post Post object.
* @param \WP_Post $post Post object.
* @param bool $skip_nested Whether to skip loading nested items (albums, tracks, etc.).
* @return array Post data.
*/
public static function get_post_data( \WP_Post $post ): array {
public static function get_post_data( \WP_Post $post, bool $skip_nested = false ): array {
// Track recursion to prevent infinite loops from shortcodes in content.
++self::$recursion_depth;
// Skip the_content filter if:
// 1. We're in a shortcode context (prevents recursive shortcode processing)
// 2. We're at depth > 1 (nested data loading)
$skip_content_filter = self::$in_shortcode_context || self::$recursion_depth > 1;
$data = array(
'id' => $post->ID,
'title' => get_the_title( $post ),
'content' => apply_filters( 'the_content', $post->post_content ),
'content' => $skip_content_filter ? wp_kses_post( $post->post_content ) : apply_filters( 'the_content', $post->post_content ),
'excerpt' => get_the_excerpt( $post ),
'permalink' => get_permalink( $post ),
'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'large' ),
@@ -206,19 +266,21 @@ class TemplateLoader {
'author' => get_the_author_meta( 'display_name', $post->post_author ),
);
// Add post type specific data.
// Add post type specific data (skip nested items if at max depth).
$load_nested = ! $skip_nested && self::$recursion_depth < self::MAX_RECURSION_DEPTH;
switch ( $post->post_type ) {
case 'fedistream_artist':
$data = array_merge( $data, self::get_artist_data( $post->ID ) );
$data = array_merge( $data, self::get_artist_data( $post->ID, $load_nested ) );
break;
case 'fedistream_album':
$data = array_merge( $data, self::get_album_data( $post->ID ) );
$data = array_merge( $data, self::get_album_data( $post->ID, $load_nested ) );
break;
case 'fedistream_track':
$data = array_merge( $data, self::get_track_data( $post->ID ) );
break;
case 'fedistream_playlist':
$data = array_merge( $data, self::get_playlist_data( $post->ID ) );
$data = array_merge( $data, self::get_playlist_data( $post->ID, $load_nested ) );
break;
}
@@ -226,16 +288,23 @@ class TemplateLoader {
$data['genres'] = self::get_terms( $post->ID, 'fedistream_genre' );
$data['moods'] = self::get_terms( $post->ID, 'fedistream_mood' );
--self::$recursion_depth;
return $data;
}
/**
* Get artist-specific data.
*
* @param int $post_id Post ID.
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested albums.
* @return array Artist data.
*/
private static function get_artist_data( int $post_id ): array {
public static function get_artist_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$type = get_post_meta( $post_id, '_fedistream_artist_type', true ) ?: 'solo';
$types = array(
'solo' => __( 'Solo Artist', 'wp-fedistream' ),
@@ -244,23 +313,48 @@ class TemplateLoader {
'collective' => __( 'Collective', 'wp-fedistream' ),
);
$albums = get_posts(
array(
'post_type' => 'fedistream_album',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_album_artist',
'meta_value' => $post_id,
'orderby' => 'meta_value',
'meta_query' => array(
array(
'key' => '_fedistream_album_release_date',
'compare' => 'EXISTS',
$albums = array();
$album_count = 0;
if ( $load_nested ) {
$album_posts = get_posts(
array(
'post_type' => 'fedistream_album',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_album_artist',
'meta_value' => $post_id,
'orderby' => 'meta_value',
'meta_query' => array(
array(
'key' => '_fedistream_album_release_date',
'compare' => 'EXISTS',
),
),
),
'order' => 'DESC',
)
);
'order' => 'DESC',
)
);
$album_count = count( $album_posts );
$albums = array_map(
function ( $album ) {
return self::get_post_data( $album, true ); // Skip further nesting.
},
$album_posts
);
} else {
// Just get the count without loading full data.
$album_count = (int) get_posts(
array(
'post_type' => 'fedistream_album',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_album_artist',
'meta_value' => $post_id,
'fields' => 'ids',
)
);
$album_count = is_array( $album_count ) ? count( $album_count ) : 0;
}
return array(
'artist_type' => $type,
@@ -270,18 +364,23 @@ class TemplateLoader {
'website' => get_post_meta( $post_id, '_fedistream_artist_website', true ),
'social_links' => get_post_meta( $post_id, '_fedistream_artist_social_links', true ) ?: array(),
'members' => get_post_meta( $post_id, '_fedistream_artist_members', true ) ?: array(),
'albums' => array_map( array( __CLASS__, 'get_post_data' ), $albums ),
'album_count' => count( $albums ),
'albums' => $albums,
'album_count' => $album_count,
);
}
/**
* Get album-specific data.
*
* @param int $post_id Post ID.
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested tracks.
* @return array Album data.
*/
private static function get_album_data( int $post_id ): array {
public static function get_album_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$type = get_post_meta( $post_id, '_fedistream_album_type', true ) ?: 'album';
$types = array(
'album' => __( 'Album', 'wp-fedistream' ),
@@ -293,24 +392,49 @@ class TemplateLoader {
);
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
$tracks = get_posts(
array(
'post_type' => 'fedistream_track',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_track_album',
'meta_value' => $post_id,
'orderby' => 'meta_value_num',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_fedistream_track_number',
'compare' => 'EXISTS',
$tracks = array();
$total_tracks = 0;
if ( $load_nested ) {
$track_posts = get_posts(
array(
'post_type' => 'fedistream_track',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_track_album',
'meta_value' => $post_id,
'orderby' => 'meta_value_num',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_fedistream_track_number',
'compare' => 'EXISTS',
),
),
),
'order' => 'ASC',
)
);
'order' => 'ASC',
)
);
$total_tracks = count( $track_posts );
$tracks = array_map(
function ( $track ) {
return self::get_post_data( $track, true ); // Skip further nesting.
},
$track_posts
);
} else {
// Just get the count without loading full data.
$track_ids = get_posts(
array(
'post_type' => 'fedistream_track',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_track_album',
'meta_value' => $post_id,
'fields' => 'ids',
)
);
$total_tracks = is_array( $track_ids ) ? count( $track_ids ) : 0;
}
return array(
'album_type' => $type,
@@ -322,19 +446,23 @@ class TemplateLoader {
'artist_url' => $artist_id ? get_permalink( $artist_id ) : '',
'upc' => get_post_meta( $post_id, '_fedistream_album_upc', true ),
'catalog_number' => get_post_meta( $post_id, '_fedistream_album_catalog_number', true ),
'total_tracks' => count( $tracks ),
'total_tracks' => $total_tracks,
'total_duration' => (int) get_post_meta( $post_id, '_fedistream_album_total_duration', true ),
'tracks' => array_map( array( __CLASS__, 'get_post_data' ), $tracks ),
'tracks' => $tracks,
);
}
/**
* Get track-specific data.
*
* @param int $post_id Post ID.
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @return array Track data.
*/
private static function get_track_data( int $post_id ): array {
public static function get_track_data( int|\WP_Post $post_id ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
$audio_file = get_post_meta( $post_id, '_fedistream_track_audio_file', true );
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true ) ?: array();
@@ -374,16 +502,21 @@ class TemplateLoader {
/**
* Get playlist-specific data.
*
* @param int $post_id Post ID.
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested tracks.
* @return array Playlist data.
*/
private static function get_playlist_data( int $post_id ): array {
public static function get_playlist_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
global $wpdb;
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
$duration = (int) get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
// Get tracks.
// Get track IDs.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$track_ids = $wpdb->get_col(
$wpdb->prepare(
@@ -392,11 +525,15 @@ class TemplateLoader {
)
);
$tracks = array();
foreach ( $track_ids as $track_id ) {
$track = get_post( $track_id );
if ( $track && 'publish' === $track->post_status ) {
$tracks[] = self::get_post_data( $track );
$tracks = array();
$track_count = count( $track_ids );
if ( $load_nested && ! empty( $track_ids ) ) {
foreach ( $track_ids as $track_id ) {
$track = get_post( $track_id );
if ( $track && 'publish' === $track->post_status ) {
$tracks[] = self::get_post_data( $track, true ); // Skip further nesting.
}
}
}
@@ -404,7 +541,7 @@ class TemplateLoader {
'visibility' => get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public',
'collaborative' => (bool) get_post_meta( $post_id, '_fedistream_playlist_collaborative', true ),
'federated' => (bool) get_post_meta( $post_id, '_fedistream_playlist_federated', true ),
'track_count' => count( $tracks ),
'track_count' => $load_nested ? count( $tracks ) : $track_count,
'total_duration' => $duration,
'duration_formatted' => $duration >= 3600
? sprintf( '%d:%02d:%02d', floor( $duration / 3600 ), floor( ( $duration % 3600 ) / 60 ), $duration % 60 )

View File

@@ -345,11 +345,18 @@ class Installer {
*/
private static function set_default_options(): void {
$defaults = array(
'wp_fedistream_enable_activitypub' => 1,
'wp_fedistream_enable_woocommerce' => 0,
'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
'wp_fedistream_max_upload_size' => 50, // MB
'wp_fedistream_default_license' => 'all-rights-reserved',
'wp_fedistream_enable_activitypub' => 1,
'wp_fedistream_enable_woocommerce' => 0,
'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
'wp_fedistream_max_upload_size' => 50, // MB
'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 ) {

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\LibraryPage;
use WP_FediStream\User\Notifications;
use WP_FediStream\License\Manager as LicenseManager;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
@@ -155,10 +156,13 @@ final class Plugin {
new ListColumns();
}
// Initialize frontend components.
if ( ! is_admin() ) {
// Initialize frontend components (only if licensed).
if ( ! is_admin() && LicenseManager::is_license_valid() ) {
new TemplateLoader();
new Shortcodes();
} elseif ( ! is_admin() ) {
// Register shortcodes that show license message.
new Shortcodes( true ); // Unlicensed mode.
}
// Initialize widgets (always needed for admin widget management).
@@ -167,8 +171,8 @@ final class Plugin {
// Initialize AJAX handlers.
new Ajax();
// Initialize ActivityPub integration.
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) ) {
// Initialize ActivityPub integration (only if licensed and enabled).
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) && LicenseManager::is_license_valid() ) {
new ActivityPubIntegration();
new ActivityPubRestApi();
}
@@ -184,6 +188,9 @@ final class Plugin {
new UserLibrary();
new LibraryPage();
new Notifications();
// Initialize license manager.
LicenseManager::get_instance();
}
/**
@@ -196,6 +203,34 @@ final class Plugin {
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
// Add settings link to plugins page.
add_filter( 'plugin_action_links_' . WP_FEDISTREAM_BASENAME, array( $this, 'add_plugin_action_links' ) );
}
/**
* Add action links to the plugins page.
*
* @param array $links Existing action links.
* @return array Modified action links.
*/
public function add_plugin_action_links( array $links ): array {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'admin.php?page=fedistream-settings' ) ),
esc_html__( 'Settings', 'wp-fedistream' )
);
$dashboard_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'admin.php?page=fedistream' ) ),
esc_html__( 'Dashboard', 'wp-fedistream' )
);
// Add our links at the beginning.
array_unshift( $links, $dashboard_link, $settings_link );
return $links;
}
/**
@@ -381,86 +416,334 @@ final class Plugin {
return;
}
// Save settings.
if ( isset( $_POST['fedistream_settings_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['fedistream_settings_nonce'] ), 'fedistream_save_settings' ) ) {
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' ) ) );
// Get current tab.
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license';
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.
$enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 );
$enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 );
$max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 );
$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">
<h1><?php esc_html_e( 'FediStream Settings', 'wp-fedistream' ); ?></h1>
<form method="post" action="">
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
<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>
<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>
</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;"><?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>
<tr>
<th scope="row">
<label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label>
</th>
<td>
<input type="number" name="max_upload_size" id="max_upload_size" value="<?php echo esc_attr( $max_upload_size ); ?>" min="1" max="500" class="small-text"> MB
<p class="description"><?php esc_html_e( 'Maximum file size for audio uploads.', 'wp-fedistream' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="default_license"><?php esc_html_e( 'Default License', 'wp-fedistream' ); ?></label>
</th>
<td>
<select name="default_license" id="default_license">
<option value="all-rights-reserved" <?php selected( $default_license, 'all-rights-reserved' ); ?>><?php esc_html_e( 'All Rights Reserved', 'wp-fedistream' ); ?></option>
<option value="cc-by" <?php selected( $default_license, 'cc-by' ); ?>>CC BY</option>
<option value="cc-by-sa" <?php selected( $default_license, 'cc-by-sa' ); ?>>CC BY-SA</option>
<option value="cc-by-nc" <?php selected( $default_license, 'cc-by-nc' ); ?>>CC BY-NC</option>
<option value="cc-by-nc-sa" <?php selected( $default_license, 'cc-by-nc-sa' ); ?>>CC BY-NC-SA</option>
<option value="cc0" <?php selected( $default_license, 'cc0' ); ?>>CC0 (Public Domain)</option>
</select>
<p class="description"><?php esc_html_e( 'Default license for new uploads.', 'wp-fedistream' ); ?></p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<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' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="license_server_url"><?php esc_html_e( 'License Server URL', 'wp-fedistream' ); ?></label>
</th>
<td>
<input type="url" name="license_server_url" id="license_server_url"
value="<?php echo esc_attr( $server_url ); ?>"
class="regular-text" placeholder="https://example.com">
<p class="description"><?php esc_html_e( 'The URL of your license server.', 'wp-fedistream' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="license_key"><?php esc_html_e( 'License Key', 'wp-fedistream' ); ?></label>
</th>
<td>
<input type="text" name="license_key" id="license_key"
value="<?php echo esc_attr( $license_key ); ?>"
class="regular-text" placeholder="XXXX-XXXX-XXXX-XXXX">
<p class="description"><?php esc_html_e( 'Your license key from your purchase.', 'wp-fedistream' ); ?></p>
</td>
</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>
<th scope="row">
<label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label>
</th>
<td>
<input type="number" name="max_upload_size" id="max_upload_size" value="<?php echo esc_attr( $max_upload_size ); ?>" min="1" max="500" class="small-text"> MB
<p class="description"><?php esc_html_e( 'Maximum file size for audio uploads.', 'wp-fedistream' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="default_license"><?php esc_html_e( 'Default License', 'wp-fedistream' ); ?></label>
</th>
<td>
<select name="default_license" id="default_license">
<option value="all-rights-reserved" <?php selected( $default_license, 'all-rights-reserved' ); ?>><?php esc_html_e( 'All Rights Reserved', 'wp-fedistream' ); ?></option>
<option value="cc-by" <?php selected( $default_license, 'cc-by' ); ?>>CC BY</option>
<option value="cc-by-sa" <?php selected( $default_license, 'cc-by-sa' ); ?>>CC BY-SA</option>
<option value="cc-by-nc" <?php selected( $default_license, 'cc-by-nc' ); ?>>CC BY-NC</option>
<option value="cc-by-nc-sa" <?php selected( $default_license, 'cc-by-nc-sa' ); ?>>CC BY-NC-SA</option>
<option value="cc0" <?php selected( $default_license, 'cc0' ); ?>>CC0 (Public Domain)</option>
</select>
<p class="description"><?php esc_html_e( 'Default license for new uploads.', 'wp-fedistream' ); ?></p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<?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
}
/**
* Enqueue admin assets.
*

View File

@@ -28,8 +28,16 @@ class Integration {
* Constructor.
*/
public function __construct() {
add_action( 'plugins_loaded', array( $this, 'check_woocommerce' ), 5 );
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
// Check WooCommerce immediately since we're instantiated during plugins_loaded.
$this->check_woocommerce();
// If plugins_loaded hasn't fully completed, hook init at priority 20.
// Otherwise, run init directly.
if ( ! did_action( 'plugins_loaded' ) || doing_action( 'plugins_loaded' ) ) {
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
} else {
$this->init();
}
}
/**

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