From 35d802c2b8c2a8ed7f789a46a37c4681607bc6ef Mon Sep 17 00:00:00 2001 From: magdev Date: Fri, 23 Jan 2026 21:18:32 +0100 Subject: [PATCH] Security improvements and API compatibility fixes (v0.3.6) - Add recursive key sorting for response signing compatibility - Fix IP header spoofing in rate limiting with trusted proxy support - Add CSRF protection to CSV export with nonce verification - Explicit Twig autoescape for XSS prevention - Escape status values in CSS classes - Update README with security documentation and trusted proxy config - Update translations for v0.3.6 Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 28 ++ README.md | 50 +++- languages/wc-licensed-product-de_CH.po | 388 +++++++++++++------------ languages/wc-licensed-product.pot | 387 ++++++++++++------------ src/Admin/AdminController.php | 15 +- src/Api/ResponseSigner.php | 31 +- src/Api/RestApiController.php | 147 +++++++++- src/Plugin.php | 1 + templates/admin/licenses.html.twig | 6 +- templates/frontend/licenses.html.twig | 4 +- wc-licensed-product.php | 4 +- 11 files changed, 669 insertions(+), 392 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5af85..04ce224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.6] - 2026-01-23 + +### Security + +- Added CSRF protection (nonce verification) to CSV export functionality +- Fixed IP header spoofing vulnerability in rate limiting - now requires explicit trusted proxy configuration +- Enabled explicit Twig autoescape for XSS protection +- Fixed unescaped status values in CSS classes in Twig templates + +### Fixed + +- Fixed response signing to use recursive key sorting for client compatibility +- ResponseSigner now recursively sorts nested array keys alphabetically as required by client implementation + +### Changed + +- Rate limiting now only trusts proxy headers when `WC_LICENSE_TRUSTED_PROXIES` constant is defined +- Added Cloudflare IP range support via `WC_LICENSE_TRUSTED_PROXIES = 'CLOUDFLARE'` configuration +- Improved IP detection with CIDR notation support for trusted proxy ranges + +### Technical Details + +- Added `recursiveKeySort()` method to `ResponseSigner` for proper response signing +- Added `isTrustedProxy()`, `isCloudflareIp()`, and `ipMatchesCidr()` methods to `RestApiController` +- Twig environment now explicitly sets `autoescape => 'html'` +- Export CSV link now includes nonce via `wp_nonce_url()` +- Added `export_csv_url()` Twig function for generating export URL with nonce + ## [0.3.5] - 2026-01-23 ### Added diff --git a/README.md b/README.md index 0fe264e..35452c9 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,13 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e - **Automatic License Generation**: License keys generated on order completion (format: XXXX-XXXX-XXXX-XXXX) - **Domain Binding**: Licenses are bound to customer-specified domains - **REST API**: Public endpoints for license validation and management +- **Response Signing**: Optional HMAC-SHA256 cryptographic signatures for API responses - **Version Binding**: Optional binding to major software versions - **Expiration Support**: Set license validity periods or lifetime licenses - **Rate Limiting**: API endpoints protected with rate limiting (30 requests/minute) +- **Trusted Proxy Support**: Configurable trusted proxies for accurate rate limiting behind CDNs - **Checkout Blocks**: Full support for WooCommerce Checkout Blocks (default since WC 8.3+) +- **Self-Licensing**: The plugin can validate its own license (for commercial distribution) ### Customer Features @@ -30,6 +33,7 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e - **License Management**: Full CRUD interface for license management - **License Dashboard**: Statistics and analytics (WooCommerce > Reports > Licenses) +- **Dashboard Widget**: License statistics on WordPress admin dashboard - **Search & Filtering**: Search by license key, domain, status, or product - **Live Search**: AJAX-powered instant search results - **Inline Editing**: Edit license status, expiry, and domain directly in the list @@ -38,7 +42,10 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e - **CSV Export/Import**: Export and import licenses via CSV - **Order Integration**: View and manage licenses directly from order pages - **Expiration Warnings**: Automatic email notifications before license expiration +- **Auto-Expire**: Daily cron job automatically expires licenses past their expiration date +- **License Testing**: Test licenses against the API directly from admin interface - **Version Management**: Manage multiple versions per product with file attachments +- **SHA256 Checksums**: File integrity verification with SHA256 hash display - **Global Settings**: Default license settings via WooCommerce settings tab - **WooCommerce HPOS**: Compatible with High-Performance Order Storage @@ -103,6 +110,40 @@ When a customer purchases a licensed product, they must enter the domain where t 3. Upload a CSV file (supports exported format or simplified format) 4. Choose options: skip header row, update existing licenses +## Security + +The plugin implements several security best practices: + +- **Input Sanitization**: All user inputs are sanitized using WordPress functions +- **Output Escaping**: All output is escaped to prevent XSS attacks +- **CSRF Protection**: Nonce verification on all forms and AJAX requests +- **SQL Injection Prevention**: All database queries use prepared statements +- **Capability Checks**: Admin functions require `manage_woocommerce` capability +- **Secure Downloads**: File downloads use hash-verified URLs with user authentication +- **Response Signing**: Optional HMAC-SHA256 signatures for API tamper protection + +### Trusted Proxy Configuration + +If your server is behind a load balancer, reverse proxy, or CDN (like Cloudflare), you need to configure trusted proxies for accurate rate limiting. Without this, the rate limiter uses the direct connection IP which may be your proxy's IP. + +**Configuration (wp-config.php):** + +```php +// For Cloudflare (includes all Cloudflare IP ranges) +define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE'); + +// For specific proxy IPs +define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.1,10.0.0.2'); + +// For CIDR ranges +define('WC_LICENSE_TRUSTED_PROXIES', '10.0.0.0/8,192.168.1.0/24'); + +// Combine multiple methods +define('WC_LICENSE_TRUSTED_PROXIES', 'CLOUDFLARE,10.0.0.1'); +``` + +**Note**: Only configure trusted proxies if you actually use them. Without this configuration, rate limiting is more secure against IP spoofing attacks. + ## REST API Full API documentation available in `openapi.json` (OpenAPI 3.1 specification). @@ -117,6 +158,12 @@ When the server is configured with a shared secret, all API responses include cr define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars'); ``` +Generate a secure secret using: + +```bash +openssl rand -hex 32 +``` + **Response Headers:** | Header | Description | @@ -256,11 +303,12 @@ Content-Type: application/json ## Email Notifications -The plugin sends automatic email notifications: +The plugin sends automatic email notifications (configurable via WooCommerce > Settings > Emails): - **Order Completion**: License keys included in order confirmation emails - **Expiration Warning (7 days)**: Reminder sent 7 days before expiration - **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration +- **License Expired**: Notification when a license auto-expires ## Changelog diff --git a/languages/wc-licensed-product-de_CH.po b/languages/wc-licensed-product-de_CH.po index 27389ee..733a49f 100644 --- a/languages/wc-licensed-product-de_CH.po +++ b/languages/wc-licensed-product-de_CH.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: WC Licensed Product 0.3.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-23 16:05+0100\n" +"POT-Creation-Date: 2026-01-23 21:09+0100\n" "PO-Revision-Date: 2026-01-22T17:15:00+00:00\n" "Last-Translator: Marco Graetsch \n" "Language-Team: German (Switzerland) \n" @@ -48,15 +48,15 @@ msgstr "beispiel.ch" #: src/Admin/OrderLicenseController.php:118 #: src/Admin/OrderLicenseController.php:182 src/Admin/AdminController.php:150 -#: src/Admin/AdminController.php:1329 src/Admin/AdminController.php:1349 -#: src/Admin/AdminController.php:1370 +#: src/Admin/AdminController.php:1340 src/Admin/AdminController.php:1360 +#: src/Admin/AdminController.php:1381 msgid "Save" msgstr "Speichern" #: src/Admin/OrderLicenseController.php:127 #: src/Admin/OrderLicenseController.php:222 src/Admin/AdminController.php:76 #: src/Admin/AdminController.php:77 src/Admin/AdminController.php:90 -#: src/Admin/AdminController.php:1189 src/Frontend/AccountController.php:90 +#: src/Admin/AdminController.php:1200 src/Frontend/AccountController.php:90 msgid "Licenses" msgstr "Lizenzen" @@ -80,41 +80,41 @@ msgstr "" "markiert wird." #: src/Admin/OrderLicenseController.php:144 -#: src/Admin/SettingsController.php:142 src/Admin/AdminController.php:1280 -#: src/Admin/AdminController.php:1431 src/Admin/AdminController.php:1480 +#: src/Admin/SettingsController.php:142 src/Admin/AdminController.php:1291 +#: src/Admin/AdminController.php:1442 src/Admin/AdminController.php:1491 #: src/Email/LicenseEmailController.php:269 msgid "License Key" msgstr "Lizenzschlüssel" -#: src/Admin/OrderLicenseController.php:145 src/Admin/AdminController.php:1281 -#: src/Admin/AdminController.php:1432 src/Admin/AdminController.php:1597 +#: src/Admin/OrderLicenseController.php:145 src/Admin/AdminController.php:1292 +#: src/Admin/AdminController.php:1443 src/Admin/AdminController.php:1608 #: src/Email/LicenseEmailController.php:268 msgid "Product" msgstr "Produkt" -#: src/Admin/OrderLicenseController.php:146 src/Admin/AdminController.php:1283 -#: src/Admin/AdminController.php:1434 src/Admin/AdminController.php:1484 +#: src/Admin/OrderLicenseController.php:146 src/Admin/AdminController.php:1294 +#: src/Admin/AdminController.php:1445 src/Admin/AdminController.php:1495 msgid "Domain" msgstr "Domain" -#: src/Admin/OrderLicenseController.php:147 src/Admin/AdminController.php:1284 -#: src/Admin/AdminController.php:1435 src/Admin/VersionAdminController.php:140 +#: src/Admin/OrderLicenseController.php:147 src/Admin/AdminController.php:1295 +#: src/Admin/AdminController.php:1446 src/Admin/VersionAdminController.php:140 msgid "Status" msgstr "Status" -#: src/Admin/OrderLicenseController.php:148 src/Admin/AdminController.php:1286 -#: src/Admin/AdminController.php:1437 src/Admin/AdminController.php:1600 -#: src/Admin/AdminController.php:1602 src/Email/LicenseEmailController.php:270 +#: src/Admin/OrderLicenseController.php:148 src/Admin/AdminController.php:1297 +#: src/Admin/AdminController.php:1448 src/Admin/AdminController.php:1611 +#: src/Admin/AdminController.php:1613 src/Email/LicenseEmailController.php:270 msgid "Expires" msgstr "Läuft ab" -#: src/Admin/OrderLicenseController.php:149 src/Admin/AdminController.php:1287 -#: src/Admin/AdminController.php:1438 src/Admin/VersionAdminController.php:142 +#: src/Admin/OrderLicenseController.php:149 src/Admin/AdminController.php:1298 +#: src/Admin/AdminController.php:1449 src/Admin/VersionAdminController.php:142 msgid "Actions" msgstr "Aktionen" #: src/Admin/OrderLicenseController.php:168 src/Admin/AdminController.php:195 -#: src/Admin/AdminController.php:1014 +#: src/Admin/AdminController.php:1019 msgid "Unknown" msgstr "Unbekannt" @@ -123,16 +123,16 @@ msgid "Edit domain" msgstr "Domain bearbeiten" #: src/Admin/OrderLicenseController.php:185 src/Admin/AdminController.php:149 -#: src/Admin/AdminController.php:1330 src/Admin/AdminController.php:1350 -#: src/Admin/AdminController.php:1371 src/Admin/AdminController.php:1526 +#: src/Admin/AdminController.php:1341 src/Admin/AdminController.php:1361 +#: src/Admin/AdminController.php:1382 src/Admin/AdminController.php:1537 #: src/Frontend/AccountController.php:271 msgid "Cancel" msgstr "Abbrechen" #: src/Admin/OrderLicenseController.php:201 #: src/Admin/SettingsController.php:192 src/Admin/AdminController.php:151 -#: src/Admin/AdminController.php:266 src/Admin/AdminController.php:1362 -#: src/Admin/AdminController.php:1602 src/Product/LicensedProductType.php:110 +#: src/Admin/AdminController.php:266 src/Admin/AdminController.php:1373 +#: src/Admin/AdminController.php:1613 src/Product/LicensedProductType.php:110 #: src/Product/LicensedProductType.php:158 msgid "Lifetime" msgstr "Lebenslang" @@ -141,6 +141,7 @@ msgstr "Lebenslang" msgid "View in Licenses" msgstr "In Lizenzen anzeigen" +#. translators: %s: Link to licenses page #: src/Admin/OrderLicenseController.php:221 #, php-format msgid "For more actions (revoke, extend, delete), go to the %s page." @@ -309,6 +310,7 @@ msgstr "" msgid "Expiration Warning Schedule" msgstr "Ablaufwarnung Zeitplan" +#. translators: %s: URL to WooCommerce email settings #: src/Admin/SettingsController.php:223 #, php-format msgid "" @@ -369,8 +371,9 @@ msgstr "Anfrage fehlgeschlagen." #: src/Admin/SettingsController.php:454 src/Admin/AdminController.php:455 #: src/Admin/AdminController.php:475 src/Admin/AdminController.php:493 #: src/Admin/AdminController.php:511 src/Admin/AdminController.php:531 -#: src/Admin/AdminController.php:549 src/Admin/AdminController.php:616 -#: src/Admin/AdminController.php:806 src/Frontend/AccountController.php:326 +#: src/Admin/AdminController.php:549 src/Admin/AdminController.php:577 +#: src/Admin/AdminController.php:621 src/Admin/AdminController.php:811 +#: src/Frontend/AccountController.php:326 msgid "Security check failed." msgstr "Sicherheitsüberprüfung fehlgeschlagen." @@ -417,8 +420,8 @@ msgstr "" "Sind Sie sicher, dass Sie diese Lizenz widerrufen möchten? Diese Aktion kann " "nicht rückgängig gemacht werden." -#: src/Admin/AdminController.php:148 src/Admin/AdminController.php:1324 -#: src/Admin/AdminController.php:1339 src/Admin/AdminController.php:1365 +#: src/Admin/AdminController.php:148 src/Admin/AdminController.php:1335 +#: src/Admin/AdminController.php:1350 src/Admin/AdminController.php:1376 msgid "Edit" msgstr "Bearbeiten" @@ -430,34 +433,34 @@ msgstr "Kopiert!" msgid "Copy failed" msgstr "Kopieren fehlgeschlagen" -#: src/Admin/AdminController.php:156 src/Admin/AdminController.php:902 -#: src/Admin/AdminController.php:1221 src/Admin/AdminController.php:1344 +#: src/Admin/AdminController.php:156 src/Admin/AdminController.php:907 +#: src/Admin/AdminController.php:1232 src/Admin/AdminController.php:1355 #: src/Admin/VersionAdminController.php:182 #: src/Admin/VersionAdminController.php:413 #: src/Admin/DashboardWidgetController.php:151 msgid "Active" msgstr "Aktiv" -#: src/Admin/AdminController.php:157 src/Admin/AdminController.php:909 -#: src/Admin/AdminController.php:1222 src/Admin/AdminController.php:1345 +#: src/Admin/AdminController.php:157 src/Admin/AdminController.php:914 +#: src/Admin/AdminController.php:1233 src/Admin/AdminController.php:1356 #: src/Admin/VersionAdminController.php:182 #: src/Admin/VersionAdminController.php:413 msgid "Inactive" msgstr "Inaktiv" -#: src/Admin/AdminController.php:158 src/Admin/AdminController.php:916 -#: src/Admin/AdminController.php:1223 src/Admin/AdminController.php:1346 +#: src/Admin/AdminController.php:158 src/Admin/AdminController.php:921 +#: src/Admin/AdminController.php:1234 src/Admin/AdminController.php:1357 #: src/Admin/DashboardWidgetController.php:159 #: src/Email/LicenseExpiredEmail.php:210 src/Email/LicenseExpiredEmail.php:259 msgid "Expired" msgstr "Abgelaufen" -#: src/Admin/AdminController.php:159 src/Admin/AdminController.php:923 -#: src/Admin/AdminController.php:1224 src/Admin/AdminController.php:1347 +#: src/Admin/AdminController.php:159 src/Admin/AdminController.php:928 +#: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1358 msgid "Revoked" msgstr "Widerrufen" -#: src/Admin/AdminController.php:196 src/Admin/AdminController.php:1018 +#: src/Admin/AdminController.php:196 src/Admin/AdminController.php:1023 msgid "Guest" msgstr "Gast" @@ -497,7 +500,7 @@ msgstr "Domain erfolgreich aktualisiert." msgid "Failed to update domain." msgstr "Domain konnte nicht aktualisiert werden." -#: src/Admin/AdminController.php:352 src/Admin/AdminController.php:1083 +#: src/Admin/AdminController.php:352 src/Admin/AdminController.php:1094 msgid "License revoked successfully." msgstr "Lizenz erfolgreich widerrufen." @@ -509,407 +512,416 @@ msgstr "Lizenz konnte nicht widerrufen werden." msgid "License key and domain are required." msgstr "Lizenzschlüssel und Domain sind erforderlich." -#: src/Admin/AdminController.php:576 +#: src/Admin/AdminController.php:581 msgid "You do not have permission to export licenses." msgstr "Sie haben keine Berechtigung, Lizenzen zu exportieren." -#: src/Admin/AdminController.php:620 +#: src/Admin/AdminController.php:625 msgid "You do not have permission to import licenses." msgstr "Sie haben keine Berechtigung, Lizenzen zu importieren." -#: src/Admin/AdminController.php:740 +#: src/Admin/AdminController.php:745 msgid "Row missing domain" msgstr "Zeile ohne Domain" -#: src/Admin/AdminController.php:744 +#: src/Admin/AdminController.php:749 msgid "Row missing valid product ID" msgstr "Zeile ohne gültige Produkt-ID" -#: src/Admin/AdminController.php:797 +#: src/Admin/AdminController.php:802 #, php-format msgid "Failed to import license for domain %s" msgstr "Import der Lizenz für Domain %s fehlgeschlagen" -#: src/Admin/AdminController.php:887 +#: src/Admin/AdminController.php:892 msgid "License Dashboard" msgstr "Lizenz-Dashboard" -#: src/Admin/AdminController.php:895 +#: src/Admin/AdminController.php:900 #: src/Admin/DashboardWidgetController.php:147 msgid "Total Licenses" msgstr "Lizenzen insgesamt" -#: src/Admin/AdminController.php:932 +#: src/Admin/AdminController.php:937 msgid "Attention:" msgstr "Achtung:" -#: src/Admin/AdminController.php:937 +#: src/Admin/AdminController.php:942 #, php-format msgid "%d license is expiring within the next 30 days." msgid_plural "%d licenses are expiring within the next 30 days." msgstr[0] "%d Lizenz läuft innerhalb der nächsten 30 Tage ab." msgstr[1] "%d Lizenzen laufen innerhalb der nächsten 30 Tage ab." -#: src/Admin/AdminController.php:945 +#: src/Admin/AdminController.php:950 msgid "View Licenses" msgstr "Lizenzen anzeigen" -#: src/Admin/AdminController.php:951 +#: src/Admin/AdminController.php:956 msgid "Quick Actions" msgstr "Schnellaktionen" -#: src/Admin/AdminController.php:955 +#: src/Admin/AdminController.php:960 msgid "Manage Licenses" msgstr "Lizenzen verwalten" -#: src/Admin/AdminController.php:959 +#: src/Admin/AdminController.php:964 msgid "Export to CSV" msgstr "Als CSV exportieren" -#: src/Admin/AdminController.php:963 wc-licensed-product.php:137 +#: src/Admin/AdminController.php:968 wc-licensed-product.php:137 msgid "Settings" msgstr "Einstellungen" -#: src/Admin/AdminController.php:1077 +#: src/Admin/AdminController.php:1088 msgid "License updated successfully." msgstr "Lizenz erfolgreich aktualisiert." -#: src/Admin/AdminController.php:1080 +#: src/Admin/AdminController.php:1091 msgid "License deleted successfully." msgstr "Lizenz erfolgreich gelöscht." -#: src/Admin/AdminController.php:1086 +#: src/Admin/AdminController.php:1097 msgid "License extended successfully." msgstr "Lizenz erfolgreich verlängert." -#: src/Admin/AdminController.php:1089 +#: src/Admin/AdminController.php:1100 msgid "License set to lifetime successfully." msgstr "Lizenz erfolgreich auf lebenslang gesetzt." -#: src/Admin/AdminController.php:1095 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1106 #, php-format msgid "%d license activated." msgid_plural "%d licenses activated." msgstr[0] "%d Lizenz aktiviert." msgstr[1] "%d Lizenzen aktiviert." -#: src/Admin/AdminController.php:1103 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1114 #, php-format msgid "%d license deactivated." msgid_plural "%d licenses deactivated." msgstr[0] "%d Lizenz deaktiviert." msgstr[1] "%d Lizenzen deaktiviert." -#: src/Admin/AdminController.php:1111 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1122 #, php-format msgid "%d license revoked." msgid_plural "%d licenses revoked." msgstr[0] "%d Lizenz widerrufen." msgstr[1] "%d Lizenzen widerrufen." -#: src/Admin/AdminController.php:1119 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1130 #, php-format msgid "%d license deleted." msgid_plural "%d licenses deleted." msgstr[0] "%d Lizenz gelöscht." msgstr[1] "%d Lizenzen gelöscht." -#: src/Admin/AdminController.php:1127 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1138 #, php-format msgid "%d license extended." msgid_plural "%d licenses extended." msgstr[0] "%d Lizenz verlängert." msgstr[1] "%d Lizenzen verlängert." -#: src/Admin/AdminController.php:1132 +#: src/Admin/AdminController.php:1143 msgid "License transferred to new domain successfully." msgstr "Lizenz erfolgreich auf neue Domain übertragen." -#: src/Admin/AdminController.php:1135 +#: src/Admin/AdminController.php:1146 msgid "Failed to transfer license. The license may be revoked or invalid." msgstr "" "Lizenzübertragung fehlgeschlagen. Die Lizenz könnte widerrufen oder ungültig " "sein." -#: src/Admin/AdminController.php:1138 +#: src/Admin/AdminController.php:1149 msgid "No licenses to export." msgstr "Keine Lizenzen zum Exportieren." -#: src/Admin/AdminController.php:1148 +#. translators: %d: number of licenses imported +#: src/Admin/AdminController.php:1159 #, php-format msgid "%d license imported." msgid_plural "%d licenses imported." msgstr[0] "%d Lizenz importiert." msgstr[1] "%d Lizenzen importiert." -#: src/Admin/AdminController.php:1155 +#. translators: %d: number of licenses updated +#: src/Admin/AdminController.php:1166 #, php-format msgid "%d updated." msgid_plural "%d updated." msgstr[0] "%d aktualisiert." msgstr[1] "%d aktualisiert." -#: src/Admin/AdminController.php:1163 +#. translators: %d: number of licenses skipped +#: src/Admin/AdminController.php:1174 #, php-format msgid "%d skipped." msgid_plural "%d skipped." msgstr[0] "%d übersprungen." msgstr[1] "%d übersprungen." -#: src/Admin/AdminController.php:1171 +#. translators: %d: number of errors +#: src/Admin/AdminController.php:1182 #, php-format msgid "%d error." msgid_plural "%d errors." msgstr[0] "%d Fehler." msgstr[1] "%d Fehler." -#: src/Admin/AdminController.php:1192 +#: src/Admin/AdminController.php:1203 msgid "Export CSV" msgstr "CSV exportieren" -#: src/Admin/AdminController.php:1196 +#: src/Admin/AdminController.php:1207 msgid "Import CSV" msgstr "CSV importieren" -#: src/Admin/AdminController.php:1211 +#: src/Admin/AdminController.php:1222 msgid "Search Licenses" msgstr "Lizenzen durchsuchen" -#: src/Admin/AdminController.php:1213 +#: src/Admin/AdminController.php:1224 msgid "Search license key or domain..." msgstr "Lizenzschlüssel oder Domain suchen..." -#: src/Admin/AdminController.php:1214 +#: src/Admin/AdminController.php:1225 msgid "Search" msgstr "Suchen" -#: src/Admin/AdminController.php:1220 +#: src/Admin/AdminController.php:1231 msgid "All Statuses" msgstr "Alle Status" -#: src/Admin/AdminController.php:1228 +#: src/Admin/AdminController.php:1239 msgid "All Products" msgstr "Alle Produkte" -#: src/Admin/AdminController.php:1234 +#: src/Admin/AdminController.php:1245 msgid "Filter" msgstr "Filtern" -#: src/Admin/AdminController.php:1237 +#: src/Admin/AdminController.php:1248 msgid "Clear" msgstr "Zurücksetzen" -#: src/Admin/AdminController.php:1242 +#: src/Admin/AdminController.php:1253 msgid "item" msgstr "Eintrag" -#: src/Admin/AdminController.php:1242 +#: src/Admin/AdminController.php:1253 msgid "items" msgstr "Einträge" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "Showing" msgstr "Anzeige" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "license" msgstr "Lizenz" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "licenses" msgstr "Lizenzen" -#: src/Admin/AdminController.php:1250 +#: src/Admin/AdminController.php:1261 msgid "filtered" msgstr "gefiltert" -#: src/Admin/AdminController.php:1252 +#: src/Admin/AdminController.php:1263 msgid "View Dashboard" msgstr "Dashboard anzeigen" -#: src/Admin/AdminController.php:1261 src/Admin/AdminController.php:1446 +#: src/Admin/AdminController.php:1272 src/Admin/AdminController.php:1457 msgid "Bulk Actions" msgstr "Massenaktionen" -#: src/Admin/AdminController.php:1262 src/Admin/AdminController.php:1447 +#: src/Admin/AdminController.php:1273 src/Admin/AdminController.php:1458 #: src/Admin/VersionAdminController.php:188 #: src/Admin/VersionAdminController.php:419 msgid "Activate" msgstr "Aktivieren" -#: src/Admin/AdminController.php:1263 src/Admin/AdminController.php:1448 +#: src/Admin/AdminController.php:1274 src/Admin/AdminController.php:1459 #: src/Admin/VersionAdminController.php:188 #: src/Admin/VersionAdminController.php:419 msgid "Deactivate" msgstr "Deaktivieren" -#: src/Admin/AdminController.php:1264 src/Admin/AdminController.php:1408 -#: src/Admin/AdminController.php:1449 +#: src/Admin/AdminController.php:1275 src/Admin/AdminController.php:1419 +#: src/Admin/AdminController.php:1460 msgid "Revoke" msgstr "Widerrufen" -#: src/Admin/AdminController.php:1265 src/Admin/AdminController.php:1450 +#: src/Admin/AdminController.php:1276 src/Admin/AdminController.php:1461 msgid "Extend 30 days" msgstr "30 Tage verlängern" -#: src/Admin/AdminController.php:1266 src/Admin/AdminController.php:1451 +#: src/Admin/AdminController.php:1277 src/Admin/AdminController.php:1462 msgid "Extend 90 days" msgstr "90 Tage verlängern" -#: src/Admin/AdminController.php:1267 src/Admin/AdminController.php:1452 +#: src/Admin/AdminController.php:1278 src/Admin/AdminController.php:1463 msgid "Extend 1 year" msgstr "1 Jahr verlängern" -#: src/Admin/AdminController.php:1268 src/Admin/AdminController.php:1417 -#: src/Admin/AdminController.php:1453 src/Admin/VersionAdminController.php:191 +#: src/Admin/AdminController.php:1279 src/Admin/AdminController.php:1428 +#: src/Admin/AdminController.php:1464 src/Admin/VersionAdminController.php:191 #: src/Admin/VersionAdminController.php:422 msgid "Delete" msgstr "Löschen" -#: src/Admin/AdminController.php:1270 src/Admin/AdminController.php:1455 +#: src/Admin/AdminController.php:1281 src/Admin/AdminController.php:1466 msgid "Apply" msgstr "Anwenden" -#: src/Admin/AdminController.php:1282 src/Admin/AdminController.php:1433 +#: src/Admin/AdminController.php:1293 src/Admin/AdminController.php:1444 #: src/Email/LicenseExpirationEmail.php:104 #: src/Email/LicenseExpiredEmail.php:96 msgid "Customer" msgstr "Kunde" -#: src/Admin/AdminController.php:1285 src/Admin/AdminController.php:1436 +#: src/Admin/AdminController.php:1296 src/Admin/AdminController.php:1447 msgid "Created" msgstr "Erstellt" -#: src/Admin/AdminController.php:1293 +#: src/Admin/AdminController.php:1304 msgid "No licenses found." msgstr "Keine Lizenzen gefunden." -#: src/Admin/AdminController.php:1303 src/Frontend/AccountController.php:194 +#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:194 msgid "Copy to clipboard" msgstr "In Zwischenablage kopieren" -#: src/Admin/AdminController.php:1369 +#: src/Admin/AdminController.php:1380 msgid "Leave empty for lifetime" msgstr "Leer lassen für lebenslang" -#: src/Admin/AdminController.php:1372 src/Admin/AdminController.php:1401 +#: src/Admin/AdminController.php:1383 src/Admin/AdminController.php:1412 msgid "Set to lifetime" msgstr "Auf lebenslang setzen" -#: src/Admin/AdminController.php:1382 +#: src/Admin/AdminController.php:1393 msgid "Test license against API" msgstr "Lizenz gegen API testen" -#: src/Admin/AdminController.php:1382 +#: src/Admin/AdminController.php:1393 msgid "Test" msgstr "Testen" -#: src/Admin/AdminController.php:1389 src/Frontend/AccountController.php:207 +#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:207 msgid "Transfer to new domain" msgstr "Auf neue Domain übertragen" -#: src/Admin/AdminController.php:1389 src/Frontend/AccountController.php:209 +#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:209 msgid "Transfer" msgstr "Übertragen" -#: src/Admin/AdminController.php:1395 +#: src/Admin/AdminController.php:1406 msgid "Extend by 30 days" msgstr "Um 30 Tage verlängern" -#: src/Admin/AdminController.php:1407 +#: src/Admin/AdminController.php:1418 msgid "Are you sure?" msgstr "Sind Sie sicher?" -#: src/Admin/AdminController.php:1416 +#: src/Admin/AdminController.php:1427 msgid "Are you sure you want to delete this license?" msgstr "Sind Sie sicher, dass Sie diese Lizenz löschen möchten?" -#: src/Admin/AdminController.php:1476 +#: src/Admin/AdminController.php:1487 msgid "License Validation Test" msgstr "Lizenzvalidierungstest" -#: src/Admin/AdminController.php:1491 +#: src/Admin/AdminController.php:1502 msgid "Testing license..." msgstr "Lizenz wird geprüft..." -#: src/Admin/AdminController.php:1497 src/Frontend/AccountController.php:249 +#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:249 msgid "Close" msgstr "Schliessen" -#: src/Admin/AdminController.php:1506 src/Frontend/AccountController.php:250 +#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:250 msgid "Transfer License to New Domain" msgstr "Lizenz auf neue Domain übertragen" -#: src/Admin/AdminController.php:1513 src/Frontend/AccountController.php:255 +#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:255 msgid "Current Domain" msgstr "Aktuelle Domain" -#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:260 +#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:260 msgid "New Domain" msgstr "Neue Domain" -#: src/Admin/AdminController.php:1520 src/Frontend/AccountController.php:264 +#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:264 msgid "Enter the new domain without http:// or www." msgstr "Geben Sie die neue Domain ohne http:// oder www ein." -#: src/Admin/AdminController.php:1525 src/Frontend/AccountController.php:269 +#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:269 msgid "Transfer License" msgstr "Lizenz übertragen" -#: src/Admin/AdminController.php:1595 +#: src/Admin/AdminController.php:1606 msgid "License is VALID" msgstr "Lizenz ist GÜLTIG" -#: src/Admin/AdminController.php:1598 src/Admin/VersionAdminController.php:81 +#: src/Admin/AdminController.php:1609 src/Admin/VersionAdminController.php:81 #: src/Admin/VersionAdminController.php:136 msgid "Version" msgstr "Version" -#: src/Admin/AdminController.php:1606 +#: src/Admin/AdminController.php:1617 msgid "License is INVALID" msgstr "Lizenz ist UNGÜLTIG" -#: src/Admin/AdminController.php:1608 +#: src/Admin/AdminController.php:1619 msgid "Error Code" msgstr "Fehlercode" -#: src/Admin/AdminController.php:1609 +#: src/Admin/AdminController.php:1620 msgid "Message" msgstr "Meldung" -#: src/Admin/AdminController.php:1622 +#: src/Admin/AdminController.php:1633 msgid "Failed to test license. Please try again." msgstr "Lizenztest fehlgeschlagen. Bitte versuchen Sie es erneut." -#: src/Admin/AdminController.php:1660 src/Admin/AdminController.php:1753 +#: src/Admin/AdminController.php:1671 src/Admin/AdminController.php:1764 msgid "Import Licenses" msgstr "Lizenzen importieren" -#: src/Admin/AdminController.php:1662 +#: src/Admin/AdminController.php:1673 msgid "Back to Licenses" msgstr "Zurück zu Lizenzen" -#: src/Admin/AdminController.php:1672 +#: src/Admin/AdminController.php:1683 msgid "Error uploading file. Please try again." msgstr "Fehler beim Hochladen der Datei. Bitte versuchen Sie es erneut." -#: src/Admin/AdminController.php:1675 +#: src/Admin/AdminController.php:1686 msgid "Invalid file type. Please upload a CSV file." msgstr "Ungültiger Dateityp. Bitte laden Sie eine CSV-Datei hoch." -#: src/Admin/AdminController.php:1678 +#: src/Admin/AdminController.php:1689 msgid "Error reading file. Please check the file format." msgstr "Fehler beim Lesen der Datei. Bitte überprüfen Sie das Dateiformat." -#: src/Admin/AdminController.php:1681 +#: src/Admin/AdminController.php:1692 msgid "An error occurred during import." msgstr "Beim Import ist ein Fehler aufgetreten." -#: src/Admin/AdminController.php:1689 +#: src/Admin/AdminController.php:1700 msgid "Import Licenses from CSV" msgstr "Lizenzen aus CSV importieren" -#: src/Admin/AdminController.php:1692 +#: src/Admin/AdminController.php:1703 msgid "" "Upload a CSV file to import licenses. You can use the exported CSV format or " "a simplified format." @@ -917,71 +929,71 @@ msgstr "" "Laden Sie eine CSV-Datei hoch, um Lizenzen zu importieren. Sie können das " "exportierte CSV-Format oder ein vereinfachtes Format verwenden." -#: src/Admin/AdminController.php:1695 +#: src/Admin/AdminController.php:1706 msgid "CSV Format" msgstr "CSV-Format" -#: src/Admin/AdminController.php:1697 +#: src/Admin/AdminController.php:1708 msgid "The CSV file should contain the following columns:" msgstr "Die CSV-Datei sollte die folgenden Spalten enthalten:" -#: src/Admin/AdminController.php:1701 +#: src/Admin/AdminController.php:1712 msgid "Full Format (from Export):" msgstr "Vollständiges Format (vom Export):" -#: src/Admin/AdminController.php:1704 +#: src/Admin/AdminController.php:1715 msgid "Simplified Format:" msgstr "Vereinfachtes Format:" -#: src/Admin/AdminController.php:1709 +#: src/Admin/AdminController.php:1720 msgid "Notes:" msgstr "Hinweise:" -#: src/Admin/AdminController.php:1710 +#: src/Admin/AdminController.php:1721 msgid "Leave License Key empty to auto-generate." msgstr "Lizenzschlüssel leer lassen für automatische Generierung." -#: src/Admin/AdminController.php:1711 +#: src/Admin/AdminController.php:1722 msgid "Status can be: active, inactive, expired, revoked (defaults to active)." msgstr "" "Status kann sein: active, inactive, expired, revoked (Standard: active)." -#: src/Admin/AdminController.php:1712 +#: src/Admin/AdminController.php:1723 msgid "Expires At should be in YYYY-MM-DD format or \"Lifetime\"." msgstr "Ablaufdatum sollte im Format JJJJ-MM-TT oder \"Lifetime\" sein." -#: src/Admin/AdminController.php:1724 +#: src/Admin/AdminController.php:1735 msgid "CSV File" msgstr "CSV-Datei" -#: src/Admin/AdminController.php:1728 +#: src/Admin/AdminController.php:1739 msgid "Select a CSV file to import." msgstr "Wählen Sie eine CSV-Datei zum Importieren." -#: src/Admin/AdminController.php:1732 +#: src/Admin/AdminController.php:1743 msgid "Options" msgstr "Optionen" -#: src/Admin/AdminController.php:1736 +#: src/Admin/AdminController.php:1747 msgid "Skip first row (header row)" msgstr "Erste Zeile überspringen (Kopfzeile)" -#: src/Admin/AdminController.php:1741 +#: src/Admin/AdminController.php:1752 msgid "Update existing licenses (by license key)" msgstr "Bestehende Lizenzen aktualisieren (nach Lizenzschlüssel)" -#: src/Admin/AdminController.php:1744 +#: src/Admin/AdminController.php:1755 msgid "" "If enabled, licenses with matching keys will be updated instead of skipped." msgstr "" "Falls aktiviert, werden Lizenzen mit übereinstimmenden Schlüsseln " "aktualisiert statt übersprungen." -#: src/Admin/AdminController.php:1771 +#: src/Admin/AdminController.php:1782 msgid "License" msgstr "Lizenz" -#: src/Admin/AdminController.php:1830 +#: src/Admin/AdminController.php:1841 msgid "No domain specified" msgstr "Keine Domain angegeben" @@ -1209,28 +1221,28 @@ msgstr "Alle Lizenzen anzeigen" msgid "Too many requests. Please try again later." msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut." -#: src/Api/RestApiController.php:222 src/Api/RestApiController.php:255 +#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378 #: src/License/LicenseManager.php:357 msgid "License key not found." msgstr "Lizenzschlüssel nicht gefunden." -#: src/Api/RestApiController.php:263 +#: src/Api/RestApiController.php:386 msgid "This license is not valid." msgstr "Diese Lizenz ist ungültig." -#: src/Api/RestApiController.php:273 +#: src/Api/RestApiController.php:396 msgid "License is already activated for this domain." msgstr "Die Lizenz ist bereits für diese Domain aktiviert." -#: src/Api/RestApiController.php:282 +#: src/Api/RestApiController.php:405 msgid "Maximum number of activations reached." msgstr "Maximale Anzahl der Aktivierungen erreicht." -#: src/Api/RestApiController.php:293 +#: src/Api/RestApiController.php:416 msgid "Failed to activate license." msgstr "Lizenz konnte nicht aktiviert werden." -#: src/Api/RestApiController.php:299 +#: src/Api/RestApiController.php:422 msgid "License activated successfully." msgstr "Lizenz erfolgreich aktiviert." @@ -1278,6 +1290,14 @@ msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz-Aktivierung ein." msgid "Domain for license activation" msgstr "Domain für Lizenz-Aktivierung" +#: src/License/PluginLicenseChecker.php:117 +msgid "License settings not configured." +msgstr "Lizenzeinstellungen nicht konfiguriert." + +#: src/License/PluginLicenseChecker.php:153 +msgid "Could not connect to license server." +msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden." + #: src/License/LicenseManager.php:366 msgid "This license has been revoked." msgstr "Diese Lizenz wurde widerrufen." @@ -1300,18 +1320,11 @@ msgstr "Diese Lizenz ist für diese Domain nicht gültig." msgid "Unknown Product" msgstr "Unbekanntes Produkt" -#: src/License/PluginLicenseChecker.php:117 -msgid "License settings not configured." -msgstr "Lizenzeinstellungen nicht konfiguriert." - -#: src/License/PluginLicenseChecker.php:153 -msgid "Could not connect to license server." -msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden." - #: src/Product/VersionManager.php:166 msgid "Attachment file not found." msgstr "Anhangs-Datei nicht gefunden." +#. translators: 1: provided hash, 2: calculated hash #: src/Product/VersionManager.php:177 #, php-format msgid "File checksum does not match. Expected: %1$s, Got: %2$s" @@ -1330,6 +1343,7 @@ msgstr "Lizenz-Einstellungen" msgid "%d days" msgstr "%d Tage" +#. translators: %s: URL to settings page #: src/Product/LicensedProductType.php:119 #, php-format msgid "Leave fields empty to use default settings from %s." @@ -1343,6 +1357,7 @@ msgstr "WooCommerce > Einstellungen > Lizensierte Produkte" msgid "Max Activations" msgstr "Max. Aktivierungen" +#. translators: %d: default max activations value #: src/Product/LicensedProductType.php:131 #, php-format msgid "Maximum number of domain activations per license. Default: %d" @@ -1352,6 +1367,7 @@ msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d" msgid "License Validity (Days)" msgstr "Lizenz-Gültigkeit (Tage)" +#. translators: %s: default validity value #: src/Product/LicensedProductType.php:149 #, php-format msgid "Number of days the license is valid. Leave empty for default (%s)." @@ -1361,6 +1377,7 @@ msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s). msgid "Bind to Major Version" msgstr "An Hauptversion binden" +#. translators: %s: default bind to version value (Yes/No) #: src/Product/LicensedProductType.php:167 #, php-format msgid "" @@ -1442,11 +1459,11 @@ msgid "You have no licenses yet." msgstr "Sie haben noch keine Lizenzen." #: src/Frontend/AccountController.php:190 +#: src/Email/LicenseExpirationEmail.php:207 +#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseEmailController.php:212 #: src/Email/LicenseEmailController.php:216 #: src/Email/LicenseEmailController.php:320 -#: src/Email/LicenseExpirationEmail.php:207 -#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256 msgid "License Key:" msgstr "Lizenzschlüssel:" @@ -1459,9 +1476,9 @@ msgid "Domain:" msgstr "Domain:" #: src/Frontend/AccountController.php:213 -#: src/Email/LicenseEmailController.php:323 #: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:272 +#: src/Email/LicenseEmailController.php:323 msgid "Expires:" msgstr "Läuft ab:" @@ -1525,25 +1542,6 @@ msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain." msgid "Failed to transfer license. Please try again." msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut." -#: src/Email/LicenseEmailController.php:256 -msgid "Your License Keys" -msgstr "Ihre Lizenzschlüssel" - -#: src/Email/LicenseEmailController.php:260 -#: src/Email/LicenseEmailController.php:315 -msgid "Licensed Domain:" -msgstr "Lizensierte Domain:" - -#: src/Email/LicenseEmailController.php:296 -#: src/Email/LicenseEmailController.php:330 -msgid "You can also view your licenses in your account under \"Licenses\"." -msgstr "" -"Sie können Ihre Lizenzen auch in Ihrem Konto unter \"Lizenzen\" einsehen." - -#: src/Email/LicenseEmailController.php:311 -msgid "YOUR LICENSE KEYS" -msgstr "IHRE LIZENZSCHLÜSSEL" - #: src/Email/LicenseExpirationEmail.php:55 msgid "License Expiration Warning" msgstr "Lizenzablauf-Warnung" @@ -1612,6 +1610,7 @@ msgstr "" "Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor " "dem Ablaufdatum." +#. translators: %s: list of placeholders #: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpiredEmail.php:288 #, php-format @@ -1658,6 +1657,25 @@ msgstr "E-Mail-Typ" msgid "Choose which format of email to send." msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll." +#: src/Email/LicenseEmailController.php:256 +msgid "Your License Keys" +msgstr "Ihre Lizenzschlüssel" + +#: src/Email/LicenseEmailController.php:260 +#: src/Email/LicenseEmailController.php:315 +msgid "Licensed Domain:" +msgstr "Lizensierte Domain:" + +#: src/Email/LicenseEmailController.php:296 +#: src/Email/LicenseEmailController.php:330 +msgid "You can also view your licenses in your account under \"Licenses\"." +msgstr "" +"Sie können Ihre Lizenzen auch in Ihrem Konto unter \"Lizenzen\" einsehen." + +#: src/Email/LicenseEmailController.php:311 +msgid "YOUR LICENSE KEYS" +msgstr "IHRE LIZENZSCHLÜSSEL" + #: src/Email/LicenseExpiredEmail.php:50 src/Email/LicenseExpiredEmail.php:76 msgid "License Expired" msgstr "Lizenz abgelaufen" @@ -1701,23 +1719,25 @@ msgstr "Status:" #: src/Email/LicenseExpiredEmail.php:278 msgid "To continue using this product, please renew your license." -msgstr "Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz." +msgstr "" +"Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz." -#: src/Plugin.php:257 +#: src/Plugin.php:258 msgid "WC Licensed Product" msgstr "WC Licensed Product" -#: src/Plugin.php:258 +#: src/Plugin.php:259 msgid "" "Plugin license is not configured or invalid. Frontend features are disabled." msgstr "" "Plugin-Lizenz ist nicht konfiguriert oder ungültig. Frontend-Funktionen sind " "deaktiviert." -#: src/Plugin.php:259 +#: src/Plugin.php:260 msgid "Configure License" msgstr "Lizenz konfigurieren" +#. translators: %s: WooCommerce plugin name #: wc-licensed-product.php:61 #, php-format msgid "%s requires WooCommerce to be installed and active." diff --git a/languages/wc-licensed-product.pot b/languages/wc-licensed-product.pot index 930d5b7..73bf4f3 100644 --- a/languages/wc-licensed-product.pot +++ b/languages/wc-licensed-product.pot @@ -1,14 +1,14 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. +# This file is distributed under the same license as the wc-licensed-product package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: wc-licensed-product 0.3.6\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-01-23 16:05+0100\n" +"POT-Creation-Date: 2026-01-23 21:09+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -49,15 +49,15 @@ msgstr "" #: src/Admin/OrderLicenseController.php:118 #: src/Admin/OrderLicenseController.php:182 src/Admin/AdminController.php:150 -#: src/Admin/AdminController.php:1329 src/Admin/AdminController.php:1349 -#: src/Admin/AdminController.php:1370 +#: src/Admin/AdminController.php:1340 src/Admin/AdminController.php:1360 +#: src/Admin/AdminController.php:1381 msgid "Save" msgstr "" #: src/Admin/OrderLicenseController.php:127 #: src/Admin/OrderLicenseController.php:222 src/Admin/AdminController.php:76 #: src/Admin/AdminController.php:77 src/Admin/AdminController.php:90 -#: src/Admin/AdminController.php:1189 src/Frontend/AccountController.php:90 +#: src/Admin/AdminController.php:1200 src/Frontend/AccountController.php:90 msgid "Licenses" msgstr "" @@ -76,41 +76,41 @@ msgid "Licenses will be generated when the order is marked as paid/completed." msgstr "" #: src/Admin/OrderLicenseController.php:144 -#: src/Admin/SettingsController.php:142 src/Admin/AdminController.php:1280 -#: src/Admin/AdminController.php:1431 src/Admin/AdminController.php:1480 +#: src/Admin/SettingsController.php:142 src/Admin/AdminController.php:1291 +#: src/Admin/AdminController.php:1442 src/Admin/AdminController.php:1491 #: src/Email/LicenseEmailController.php:269 msgid "License Key" msgstr "" -#: src/Admin/OrderLicenseController.php:145 src/Admin/AdminController.php:1281 -#: src/Admin/AdminController.php:1432 src/Admin/AdminController.php:1597 +#: src/Admin/OrderLicenseController.php:145 src/Admin/AdminController.php:1292 +#: src/Admin/AdminController.php:1443 src/Admin/AdminController.php:1608 #: src/Email/LicenseEmailController.php:268 msgid "Product" msgstr "" -#: src/Admin/OrderLicenseController.php:146 src/Admin/AdminController.php:1283 -#: src/Admin/AdminController.php:1434 src/Admin/AdminController.php:1484 +#: src/Admin/OrderLicenseController.php:146 src/Admin/AdminController.php:1294 +#: src/Admin/AdminController.php:1445 src/Admin/AdminController.php:1495 msgid "Domain" msgstr "" -#: src/Admin/OrderLicenseController.php:147 src/Admin/AdminController.php:1284 -#: src/Admin/AdminController.php:1435 src/Admin/VersionAdminController.php:140 +#: src/Admin/OrderLicenseController.php:147 src/Admin/AdminController.php:1295 +#: src/Admin/AdminController.php:1446 src/Admin/VersionAdminController.php:140 msgid "Status" msgstr "" -#: src/Admin/OrderLicenseController.php:148 src/Admin/AdminController.php:1286 -#: src/Admin/AdminController.php:1437 src/Admin/AdminController.php:1600 -#: src/Admin/AdminController.php:1602 src/Email/LicenseEmailController.php:270 +#: src/Admin/OrderLicenseController.php:148 src/Admin/AdminController.php:1297 +#: src/Admin/AdminController.php:1448 src/Admin/AdminController.php:1611 +#: src/Admin/AdminController.php:1613 src/Email/LicenseEmailController.php:270 msgid "Expires" msgstr "" -#: src/Admin/OrderLicenseController.php:149 src/Admin/AdminController.php:1287 -#: src/Admin/AdminController.php:1438 src/Admin/VersionAdminController.php:142 +#: src/Admin/OrderLicenseController.php:149 src/Admin/AdminController.php:1298 +#: src/Admin/AdminController.php:1449 src/Admin/VersionAdminController.php:142 msgid "Actions" msgstr "" #: src/Admin/OrderLicenseController.php:168 src/Admin/AdminController.php:195 -#: src/Admin/AdminController.php:1014 +#: src/Admin/AdminController.php:1019 msgid "Unknown" msgstr "" @@ -119,16 +119,16 @@ msgid "Edit domain" msgstr "" #: src/Admin/OrderLicenseController.php:185 src/Admin/AdminController.php:149 -#: src/Admin/AdminController.php:1330 src/Admin/AdminController.php:1350 -#: src/Admin/AdminController.php:1371 src/Admin/AdminController.php:1526 +#: src/Admin/AdminController.php:1341 src/Admin/AdminController.php:1361 +#: src/Admin/AdminController.php:1382 src/Admin/AdminController.php:1537 #: src/Frontend/AccountController.php:271 msgid "Cancel" msgstr "" #: src/Admin/OrderLicenseController.php:201 #: src/Admin/SettingsController.php:192 src/Admin/AdminController.php:151 -#: src/Admin/AdminController.php:266 src/Admin/AdminController.php:1362 -#: src/Admin/AdminController.php:1602 src/Product/LicensedProductType.php:110 +#: src/Admin/AdminController.php:266 src/Admin/AdminController.php:1373 +#: src/Admin/AdminController.php:1613 src/Product/LicensedProductType.php:110 #: src/Product/LicensedProductType.php:158 msgid "Lifetime" msgstr "" @@ -137,6 +137,7 @@ msgstr "" msgid "View in Licenses" msgstr "" +#. translators: %s: Link to licenses page #: src/Admin/OrderLicenseController.php:221 #, php-format msgid "For more actions (revoke, extend, delete), go to the %s page." @@ -293,6 +294,7 @@ msgstr "" msgid "Expiration Warning Schedule" msgstr "" +#. translators: %s: URL to WooCommerce email settings #: src/Admin/SettingsController.php:223 #, php-format msgid "" @@ -348,8 +350,9 @@ msgstr "" #: src/Admin/SettingsController.php:454 src/Admin/AdminController.php:455 #: src/Admin/AdminController.php:475 src/Admin/AdminController.php:493 #: src/Admin/AdminController.php:511 src/Admin/AdminController.php:531 -#: src/Admin/AdminController.php:549 src/Admin/AdminController.php:616 -#: src/Admin/AdminController.php:806 src/Frontend/AccountController.php:326 +#: src/Admin/AdminController.php:549 src/Admin/AdminController.php:577 +#: src/Admin/AdminController.php:621 src/Admin/AdminController.php:811 +#: src/Frontend/AccountController.php:326 msgid "Security check failed." msgstr "" @@ -394,8 +397,8 @@ msgid "" "Are you sure you want to revoke this license? This action cannot be undone." msgstr "" -#: src/Admin/AdminController.php:148 src/Admin/AdminController.php:1324 -#: src/Admin/AdminController.php:1339 src/Admin/AdminController.php:1365 +#: src/Admin/AdminController.php:148 src/Admin/AdminController.php:1335 +#: src/Admin/AdminController.php:1350 src/Admin/AdminController.php:1376 msgid "Edit" msgstr "" @@ -407,34 +410,34 @@ msgstr "" msgid "Copy failed" msgstr "" -#: src/Admin/AdminController.php:156 src/Admin/AdminController.php:902 -#: src/Admin/AdminController.php:1221 src/Admin/AdminController.php:1344 +#: src/Admin/AdminController.php:156 src/Admin/AdminController.php:907 +#: src/Admin/AdminController.php:1232 src/Admin/AdminController.php:1355 #: src/Admin/VersionAdminController.php:182 #: src/Admin/VersionAdminController.php:413 #: src/Admin/DashboardWidgetController.php:151 msgid "Active" msgstr "" -#: src/Admin/AdminController.php:157 src/Admin/AdminController.php:909 -#: src/Admin/AdminController.php:1222 src/Admin/AdminController.php:1345 +#: src/Admin/AdminController.php:157 src/Admin/AdminController.php:914 +#: src/Admin/AdminController.php:1233 src/Admin/AdminController.php:1356 #: src/Admin/VersionAdminController.php:182 #: src/Admin/VersionAdminController.php:413 msgid "Inactive" msgstr "" -#: src/Admin/AdminController.php:158 src/Admin/AdminController.php:916 -#: src/Admin/AdminController.php:1223 src/Admin/AdminController.php:1346 +#: src/Admin/AdminController.php:158 src/Admin/AdminController.php:921 +#: src/Admin/AdminController.php:1234 src/Admin/AdminController.php:1357 #: src/Admin/DashboardWidgetController.php:159 #: src/Email/LicenseExpiredEmail.php:210 src/Email/LicenseExpiredEmail.php:259 msgid "Expired" msgstr "" -#: src/Admin/AdminController.php:159 src/Admin/AdminController.php:923 -#: src/Admin/AdminController.php:1224 src/Admin/AdminController.php:1347 +#: src/Admin/AdminController.php:159 src/Admin/AdminController.php:928 +#: src/Admin/AdminController.php:1235 src/Admin/AdminController.php:1358 msgid "Revoked" msgstr "" -#: src/Admin/AdminController.php:196 src/Admin/AdminController.php:1018 +#: src/Admin/AdminController.php:196 src/Admin/AdminController.php:1023 msgid "Guest" msgstr "" @@ -474,7 +477,7 @@ msgstr "" msgid "Failed to update domain." msgstr "" -#: src/Admin/AdminController.php:352 src/Admin/AdminController.php:1083 +#: src/Admin/AdminController.php:352 src/Admin/AdminController.php:1094 msgid "License revoked successfully." msgstr "" @@ -486,472 +489,481 @@ msgstr "" msgid "License key and domain are required." msgstr "" -#: src/Admin/AdminController.php:576 +#: src/Admin/AdminController.php:581 msgid "You do not have permission to export licenses." msgstr "" -#: src/Admin/AdminController.php:620 +#: src/Admin/AdminController.php:625 msgid "You do not have permission to import licenses." msgstr "" -#: src/Admin/AdminController.php:740 +#: src/Admin/AdminController.php:745 msgid "Row missing domain" msgstr "" -#: src/Admin/AdminController.php:744 +#: src/Admin/AdminController.php:749 msgid "Row missing valid product ID" msgstr "" -#: src/Admin/AdminController.php:797 +#: src/Admin/AdminController.php:802 #, php-format msgid "Failed to import license for domain %s" msgstr "" -#: src/Admin/AdminController.php:887 +#: src/Admin/AdminController.php:892 msgid "License Dashboard" msgstr "" -#: src/Admin/AdminController.php:895 +#: src/Admin/AdminController.php:900 #: src/Admin/DashboardWidgetController.php:147 msgid "Total Licenses" msgstr "" -#: src/Admin/AdminController.php:932 +#: src/Admin/AdminController.php:937 msgid "Attention:" msgstr "" -#: src/Admin/AdminController.php:937 +#: src/Admin/AdminController.php:942 #, php-format msgid "%d license is expiring within the next 30 days." msgid_plural "%d licenses are expiring within the next 30 days." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:945 +#: src/Admin/AdminController.php:950 msgid "View Licenses" msgstr "" -#: src/Admin/AdminController.php:951 +#: src/Admin/AdminController.php:956 msgid "Quick Actions" msgstr "" -#: src/Admin/AdminController.php:955 +#: src/Admin/AdminController.php:960 msgid "Manage Licenses" msgstr "" -#: src/Admin/AdminController.php:959 +#: src/Admin/AdminController.php:964 msgid "Export to CSV" msgstr "" -#: src/Admin/AdminController.php:963 wc-licensed-product.php:137 +#: src/Admin/AdminController.php:968 wc-licensed-product.php:137 msgid "Settings" msgstr "" -#: src/Admin/AdminController.php:1077 +#: src/Admin/AdminController.php:1088 msgid "License updated successfully." msgstr "" -#: src/Admin/AdminController.php:1080 +#: src/Admin/AdminController.php:1091 msgid "License deleted successfully." msgstr "" -#: src/Admin/AdminController.php:1086 +#: src/Admin/AdminController.php:1097 msgid "License extended successfully." msgstr "" -#: src/Admin/AdminController.php:1089 +#: src/Admin/AdminController.php:1100 msgid "License set to lifetime successfully." msgstr "" -#: src/Admin/AdminController.php:1095 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1106 #, php-format msgid "%d license activated." msgid_plural "%d licenses activated." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1103 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1114 #, php-format msgid "%d license deactivated." msgid_plural "%d licenses deactivated." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1111 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1122 #, php-format msgid "%d license revoked." msgid_plural "%d licenses revoked." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1119 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1130 #, php-format msgid "%d license deleted." msgid_plural "%d licenses deleted." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1127 +#. translators: %d: number of licenses +#: src/Admin/AdminController.php:1138 #, php-format msgid "%d license extended." msgid_plural "%d licenses extended." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1132 +#: src/Admin/AdminController.php:1143 msgid "License transferred to new domain successfully." msgstr "" -#: src/Admin/AdminController.php:1135 +#: src/Admin/AdminController.php:1146 msgid "Failed to transfer license. The license may be revoked or invalid." msgstr "" -#: src/Admin/AdminController.php:1138 +#: src/Admin/AdminController.php:1149 msgid "No licenses to export." msgstr "" -#: src/Admin/AdminController.php:1148 +#. translators: %d: number of licenses imported +#: src/Admin/AdminController.php:1159 #, php-format msgid "%d license imported." msgid_plural "%d licenses imported." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1155 +#. translators: %d: number of licenses updated +#: src/Admin/AdminController.php:1166 #, php-format msgid "%d updated." msgid_plural "%d updated." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1163 +#. translators: %d: number of licenses skipped +#: src/Admin/AdminController.php:1174 #, php-format msgid "%d skipped." msgid_plural "%d skipped." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1171 +#. translators: %d: number of errors +#: src/Admin/AdminController.php:1182 #, php-format msgid "%d error." msgid_plural "%d errors." msgstr[0] "" msgstr[1] "" -#: src/Admin/AdminController.php:1192 +#: src/Admin/AdminController.php:1203 msgid "Export CSV" msgstr "" -#: src/Admin/AdminController.php:1196 +#: src/Admin/AdminController.php:1207 msgid "Import CSV" msgstr "" -#: src/Admin/AdminController.php:1211 +#: src/Admin/AdminController.php:1222 msgid "Search Licenses" msgstr "" -#: src/Admin/AdminController.php:1213 +#: src/Admin/AdminController.php:1224 msgid "Search license key or domain..." msgstr "" -#: src/Admin/AdminController.php:1214 +#: src/Admin/AdminController.php:1225 msgid "Search" msgstr "" -#: src/Admin/AdminController.php:1220 +#: src/Admin/AdminController.php:1231 msgid "All Statuses" msgstr "" -#: src/Admin/AdminController.php:1228 +#: src/Admin/AdminController.php:1239 msgid "All Products" msgstr "" -#: src/Admin/AdminController.php:1234 +#: src/Admin/AdminController.php:1245 msgid "Filter" msgstr "" -#: src/Admin/AdminController.php:1237 +#: src/Admin/AdminController.php:1248 msgid "Clear" msgstr "" -#: src/Admin/AdminController.php:1242 +#: src/Admin/AdminController.php:1253 msgid "item" msgstr "" -#: src/Admin/AdminController.php:1242 +#: src/Admin/AdminController.php:1253 msgid "items" msgstr "" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "Showing" msgstr "" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "license" msgstr "" -#: src/Admin/AdminController.php:1248 +#: src/Admin/AdminController.php:1259 msgid "licenses" msgstr "" -#: src/Admin/AdminController.php:1250 +#: src/Admin/AdminController.php:1261 msgid "filtered" msgstr "" -#: src/Admin/AdminController.php:1252 +#: src/Admin/AdminController.php:1263 msgid "View Dashboard" msgstr "" -#: src/Admin/AdminController.php:1261 src/Admin/AdminController.php:1446 +#: src/Admin/AdminController.php:1272 src/Admin/AdminController.php:1457 msgid "Bulk Actions" msgstr "" -#: src/Admin/AdminController.php:1262 src/Admin/AdminController.php:1447 +#: src/Admin/AdminController.php:1273 src/Admin/AdminController.php:1458 #: src/Admin/VersionAdminController.php:188 #: src/Admin/VersionAdminController.php:419 msgid "Activate" msgstr "" -#: src/Admin/AdminController.php:1263 src/Admin/AdminController.php:1448 +#: src/Admin/AdminController.php:1274 src/Admin/AdminController.php:1459 #: src/Admin/VersionAdminController.php:188 #: src/Admin/VersionAdminController.php:419 msgid "Deactivate" msgstr "" -#: src/Admin/AdminController.php:1264 src/Admin/AdminController.php:1408 -#: src/Admin/AdminController.php:1449 +#: src/Admin/AdminController.php:1275 src/Admin/AdminController.php:1419 +#: src/Admin/AdminController.php:1460 msgid "Revoke" msgstr "" -#: src/Admin/AdminController.php:1265 src/Admin/AdminController.php:1450 +#: src/Admin/AdminController.php:1276 src/Admin/AdminController.php:1461 msgid "Extend 30 days" msgstr "" -#: src/Admin/AdminController.php:1266 src/Admin/AdminController.php:1451 +#: src/Admin/AdminController.php:1277 src/Admin/AdminController.php:1462 msgid "Extend 90 days" msgstr "" -#: src/Admin/AdminController.php:1267 src/Admin/AdminController.php:1452 +#: src/Admin/AdminController.php:1278 src/Admin/AdminController.php:1463 msgid "Extend 1 year" msgstr "" -#: src/Admin/AdminController.php:1268 src/Admin/AdminController.php:1417 -#: src/Admin/AdminController.php:1453 src/Admin/VersionAdminController.php:191 +#: src/Admin/AdminController.php:1279 src/Admin/AdminController.php:1428 +#: src/Admin/AdminController.php:1464 src/Admin/VersionAdminController.php:191 #: src/Admin/VersionAdminController.php:422 msgid "Delete" msgstr "" -#: src/Admin/AdminController.php:1270 src/Admin/AdminController.php:1455 +#: src/Admin/AdminController.php:1281 src/Admin/AdminController.php:1466 msgid "Apply" msgstr "" -#: src/Admin/AdminController.php:1282 src/Admin/AdminController.php:1433 +#: src/Admin/AdminController.php:1293 src/Admin/AdminController.php:1444 #: src/Email/LicenseExpirationEmail.php:104 #: src/Email/LicenseExpiredEmail.php:96 msgid "Customer" msgstr "" -#: src/Admin/AdminController.php:1285 src/Admin/AdminController.php:1436 +#: src/Admin/AdminController.php:1296 src/Admin/AdminController.php:1447 msgid "Created" msgstr "" -#: src/Admin/AdminController.php:1293 +#: src/Admin/AdminController.php:1304 msgid "No licenses found." msgstr "" -#: src/Admin/AdminController.php:1303 src/Frontend/AccountController.php:194 +#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:194 msgid "Copy to clipboard" msgstr "" -#: src/Admin/AdminController.php:1369 +#: src/Admin/AdminController.php:1380 msgid "Leave empty for lifetime" msgstr "" -#: src/Admin/AdminController.php:1372 src/Admin/AdminController.php:1401 +#: src/Admin/AdminController.php:1383 src/Admin/AdminController.php:1412 msgid "Set to lifetime" msgstr "" -#: src/Admin/AdminController.php:1382 +#: src/Admin/AdminController.php:1393 msgid "Test license against API" msgstr "" -#: src/Admin/AdminController.php:1382 +#: src/Admin/AdminController.php:1393 msgid "Test" msgstr "" -#: src/Admin/AdminController.php:1389 src/Frontend/AccountController.php:207 +#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:207 msgid "Transfer to new domain" msgstr "" -#: src/Admin/AdminController.php:1389 src/Frontend/AccountController.php:209 +#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:209 msgid "Transfer" msgstr "" -#: src/Admin/AdminController.php:1395 +#: src/Admin/AdminController.php:1406 msgid "Extend by 30 days" msgstr "" -#: src/Admin/AdminController.php:1407 +#: src/Admin/AdminController.php:1418 msgid "Are you sure?" msgstr "" -#: src/Admin/AdminController.php:1416 +#: src/Admin/AdminController.php:1427 msgid "Are you sure you want to delete this license?" msgstr "" -#: src/Admin/AdminController.php:1476 +#: src/Admin/AdminController.php:1487 msgid "License Validation Test" msgstr "" -#: src/Admin/AdminController.php:1491 +#: src/Admin/AdminController.php:1502 msgid "Testing license..." msgstr "" -#: src/Admin/AdminController.php:1497 src/Frontend/AccountController.php:249 +#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:249 msgid "Close" msgstr "" -#: src/Admin/AdminController.php:1506 src/Frontend/AccountController.php:250 +#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:250 msgid "Transfer License to New Domain" msgstr "" -#: src/Admin/AdminController.php:1513 src/Frontend/AccountController.php:255 +#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:255 msgid "Current Domain" msgstr "" -#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:260 +#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:260 msgid "New Domain" msgstr "" -#: src/Admin/AdminController.php:1520 src/Frontend/AccountController.php:264 +#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:264 msgid "Enter the new domain without http:// or www." msgstr "" -#: src/Admin/AdminController.php:1525 src/Frontend/AccountController.php:269 +#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:269 msgid "Transfer License" msgstr "" -#: src/Admin/AdminController.php:1595 +#: src/Admin/AdminController.php:1606 msgid "License is VALID" msgstr "" -#: src/Admin/AdminController.php:1598 src/Admin/VersionAdminController.php:81 +#: src/Admin/AdminController.php:1609 src/Admin/VersionAdminController.php:81 #: src/Admin/VersionAdminController.php:136 msgid "Version" msgstr "" -#: src/Admin/AdminController.php:1606 +#: src/Admin/AdminController.php:1617 msgid "License is INVALID" msgstr "" -#: src/Admin/AdminController.php:1608 +#: src/Admin/AdminController.php:1619 msgid "Error Code" msgstr "" -#: src/Admin/AdminController.php:1609 +#: src/Admin/AdminController.php:1620 msgid "Message" msgstr "" -#: src/Admin/AdminController.php:1622 +#: src/Admin/AdminController.php:1633 msgid "Failed to test license. Please try again." msgstr "" -#: src/Admin/AdminController.php:1660 src/Admin/AdminController.php:1753 +#: src/Admin/AdminController.php:1671 src/Admin/AdminController.php:1764 msgid "Import Licenses" msgstr "" -#: src/Admin/AdminController.php:1662 +#: src/Admin/AdminController.php:1673 msgid "Back to Licenses" msgstr "" -#: src/Admin/AdminController.php:1672 +#: src/Admin/AdminController.php:1683 msgid "Error uploading file. Please try again." msgstr "" -#: src/Admin/AdminController.php:1675 +#: src/Admin/AdminController.php:1686 msgid "Invalid file type. Please upload a CSV file." msgstr "" -#: src/Admin/AdminController.php:1678 +#: src/Admin/AdminController.php:1689 msgid "Error reading file. Please check the file format." msgstr "" -#: src/Admin/AdminController.php:1681 +#: src/Admin/AdminController.php:1692 msgid "An error occurred during import." msgstr "" -#: src/Admin/AdminController.php:1689 +#: src/Admin/AdminController.php:1700 msgid "Import Licenses from CSV" msgstr "" -#: src/Admin/AdminController.php:1692 +#: src/Admin/AdminController.php:1703 msgid "" "Upload a CSV file to import licenses. You can use the exported CSV format or " "a simplified format." msgstr "" -#: src/Admin/AdminController.php:1695 +#: src/Admin/AdminController.php:1706 msgid "CSV Format" msgstr "" -#: src/Admin/AdminController.php:1697 +#: src/Admin/AdminController.php:1708 msgid "The CSV file should contain the following columns:" msgstr "" -#: src/Admin/AdminController.php:1701 +#: src/Admin/AdminController.php:1712 msgid "Full Format (from Export):" msgstr "" -#: src/Admin/AdminController.php:1704 +#: src/Admin/AdminController.php:1715 msgid "Simplified Format:" msgstr "" -#: src/Admin/AdminController.php:1709 +#: src/Admin/AdminController.php:1720 msgid "Notes:" msgstr "" -#: src/Admin/AdminController.php:1710 +#: src/Admin/AdminController.php:1721 msgid "Leave License Key empty to auto-generate." msgstr "" -#: src/Admin/AdminController.php:1711 +#: src/Admin/AdminController.php:1722 msgid "Status can be: active, inactive, expired, revoked (defaults to active)." msgstr "" -#: src/Admin/AdminController.php:1712 +#: src/Admin/AdminController.php:1723 msgid "Expires At should be in YYYY-MM-DD format or \"Lifetime\"." msgstr "" -#: src/Admin/AdminController.php:1724 +#: src/Admin/AdminController.php:1735 msgid "CSV File" msgstr "" -#: src/Admin/AdminController.php:1728 +#: src/Admin/AdminController.php:1739 msgid "Select a CSV file to import." msgstr "" -#: src/Admin/AdminController.php:1732 +#: src/Admin/AdminController.php:1743 msgid "Options" msgstr "" -#: src/Admin/AdminController.php:1736 +#: src/Admin/AdminController.php:1747 msgid "Skip first row (header row)" msgstr "" -#: src/Admin/AdminController.php:1741 +#: src/Admin/AdminController.php:1752 msgid "Update existing licenses (by license key)" msgstr "" -#: src/Admin/AdminController.php:1744 +#: src/Admin/AdminController.php:1755 msgid "" "If enabled, licenses with matching keys will be updated instead of skipped." msgstr "" -#: src/Admin/AdminController.php:1771 +#: src/Admin/AdminController.php:1782 msgid "License" msgstr "" -#: src/Admin/AdminController.php:1830 +#: src/Admin/AdminController.php:1841 msgid "No domain specified" msgstr "" @@ -1171,28 +1183,28 @@ msgstr "" msgid "Too many requests. Please try again later." msgstr "" -#: src/Api/RestApiController.php:222 src/Api/RestApiController.php:255 +#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378 #: src/License/LicenseManager.php:357 msgid "License key not found." msgstr "" -#: src/Api/RestApiController.php:263 +#: src/Api/RestApiController.php:386 msgid "This license is not valid." msgstr "" -#: src/Api/RestApiController.php:273 +#: src/Api/RestApiController.php:396 msgid "License is already activated for this domain." msgstr "" -#: src/Api/RestApiController.php:282 +#: src/Api/RestApiController.php:405 msgid "Maximum number of activations reached." msgstr "" -#: src/Api/RestApiController.php:293 +#: src/Api/RestApiController.php:416 msgid "Failed to activate license." msgstr "" -#: src/Api/RestApiController.php:299 +#: src/Api/RestApiController.php:422 msgid "License activated successfully." msgstr "" @@ -1238,6 +1250,14 @@ msgstr "" msgid "Domain for license activation" msgstr "" +#: src/License/PluginLicenseChecker.php:117 +msgid "License settings not configured." +msgstr "" + +#: src/License/PluginLicenseChecker.php:153 +msgid "Could not connect to license server." +msgstr "" + #: src/License/LicenseManager.php:366 msgid "This license has been revoked." msgstr "" @@ -1260,18 +1280,11 @@ msgstr "" msgid "Unknown Product" msgstr "" -#: src/License/PluginLicenseChecker.php:117 -msgid "License settings not configured." -msgstr "" - -#: src/License/PluginLicenseChecker.php:153 -msgid "Could not connect to license server." -msgstr "" - #: src/Product/VersionManager.php:166 msgid "Attachment file not found." msgstr "" +#. translators: 1: provided hash, 2: calculated hash #: src/Product/VersionManager.php:177 #, php-format msgid "File checksum does not match. Expected: %1$s, Got: %2$s" @@ -1290,6 +1303,7 @@ msgstr "" msgid "%d days" msgstr "" +#. translators: %s: URL to settings page #: src/Product/LicensedProductType.php:119 #, php-format msgid "Leave fields empty to use default settings from %s." @@ -1303,6 +1317,7 @@ msgstr "" msgid "Max Activations" msgstr "" +#. translators: %d: default max activations value #: src/Product/LicensedProductType.php:131 #, php-format msgid "Maximum number of domain activations per license. Default: %d" @@ -1312,6 +1327,7 @@ msgstr "" msgid "License Validity (Days)" msgstr "" +#. translators: %s: default validity value #: src/Product/LicensedProductType.php:149 #, php-format msgid "Number of days the license is valid. Leave empty for default (%s)." @@ -1321,6 +1337,7 @@ msgstr "" msgid "Bind to Major Version" msgstr "" +#. translators: %s: default bind to version value (Yes/No) #: src/Product/LicensedProductType.php:167 #, php-format msgid "" @@ -1400,11 +1417,11 @@ msgid "You have no licenses yet." msgstr "" #: src/Frontend/AccountController.php:190 +#: src/Email/LicenseExpirationEmail.php:207 +#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseEmailController.php:212 #: src/Email/LicenseEmailController.php:216 #: src/Email/LicenseEmailController.php:320 -#: src/Email/LicenseExpirationEmail.php:207 -#: src/Email/LicenseExpirationEmail.php:270 #: src/Email/LicenseExpiredEmail.php:191 src/Email/LicenseExpiredEmail.php:256 msgid "License Key:" msgstr "" @@ -1417,9 +1434,9 @@ msgid "Domain:" msgstr "" #: src/Frontend/AccountController.php:213 -#: src/Email/LicenseEmailController.php:323 #: src/Email/LicenseExpirationEmail.php:219 #: src/Email/LicenseExpirationEmail.php:272 +#: src/Email/LicenseEmailController.php:323 msgid "Expires:" msgstr "" @@ -1481,24 +1498,6 @@ msgstr "" msgid "Failed to transfer license. Please try again." msgstr "" -#: src/Email/LicenseEmailController.php:256 -msgid "Your License Keys" -msgstr "" - -#: src/Email/LicenseEmailController.php:260 -#: src/Email/LicenseEmailController.php:315 -msgid "Licensed Domain:" -msgstr "" - -#: src/Email/LicenseEmailController.php:296 -#: src/Email/LicenseEmailController.php:330 -msgid "You can also view your licenses in your account under \"Licenses\"." -msgstr "" - -#: src/Email/LicenseEmailController.php:311 -msgid "YOUR LICENSE KEYS" -msgstr "" - #: src/Email/LicenseExpirationEmail.php:55 msgid "License Expiration Warning" msgstr "" @@ -1561,6 +1560,7 @@ msgid "" "expiration date." msgstr "" +#. translators: %s: list of placeholders #: src/Email/LicenseExpirationEmail.php:301 #: src/Email/LicenseExpiredEmail.php:288 #, php-format @@ -1607,6 +1607,24 @@ msgstr "" msgid "Choose which format of email to send." msgstr "" +#: src/Email/LicenseEmailController.php:256 +msgid "Your License Keys" +msgstr "" + +#: src/Email/LicenseEmailController.php:260 +#: src/Email/LicenseEmailController.php:315 +msgid "Licensed Domain:" +msgstr "" + +#: src/Email/LicenseEmailController.php:296 +#: src/Email/LicenseEmailController.php:330 +msgid "You can also view your licenses in your account under \"Licenses\"." +msgstr "" + +#: src/Email/LicenseEmailController.php:311 +msgid "YOUR LICENSE KEYS" +msgstr "" + #: src/Email/LicenseExpiredEmail.php:50 src/Email/LicenseExpiredEmail.php:76 msgid "License Expired" msgstr "" @@ -1648,19 +1666,20 @@ msgstr "" msgid "To continue using this product, please renew your license." msgstr "" -#: src/Plugin.php:257 +#: src/Plugin.php:258 msgid "WC Licensed Product" msgstr "" -#: src/Plugin.php:258 +#: src/Plugin.php:259 msgid "" "Plugin license is not configured or invalid. Frontend features are disabled." msgstr "" -#: src/Plugin.php:259 +#: src/Plugin.php:260 msgid "Configure License" msgstr "" +#. translators: %s: WooCommerce plugin name #: wc-licensed-product.php:61 #, php-format msgid "%s requires WooCommerce to be installed and active." diff --git a/src/Admin/AdminController.php b/src/Admin/AdminController.php index 7d00734..4903a2b 100644 --- a/src/Admin/AdminController.php +++ b/src/Admin/AdminController.php @@ -572,6 +572,11 @@ final class AdminController */ private function handleCsvExport(): void { + // Verify nonce for CSRF protection + if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'export_licenses_csv')) { + wp_die(__('Security check failed.', 'wc-licensed-product')); + } + if (!current_user_can('manage_woocommerce')) { wp_die(__('You do not have permission to export licenses.', 'wc-licensed-product')); } @@ -954,7 +959,7 @@ final class AdminController - + @@ -1048,6 +1053,12 @@ final class AdminController $this->twig->addFunction(new \Twig\TwigFunction('transfer_nonce', function (): string { return wp_create_nonce('transfer_license'); })); + $this->twig->addFunction(new \Twig\TwigFunction('export_csv_url', function (): string { + return wp_nonce_url( + admin_url('admin.php?page=wc-licenses&action=export_csv'), + 'export_licenses_csv' + ); + })); try { echo $this->twig->render('admin/licenses.html.twig', [ @@ -1187,7 +1198,7 @@ final class AdminController ?>

- + diff --git a/src/Api/ResponseSigner.php b/src/Api/ResponseSigner.php index d1e085e..0d81ece 100644 --- a/src/Api/ResponseSigner.php +++ b/src/Api/ResponseSigner.php @@ -94,8 +94,8 @@ final class ResponseSigner $timestamp = time(); $signingKey = $this->deriveKey($licenseKey); - // Sort keys for consistent ordering - ksort($data); + // Recursively sort keys for consistent ordering (required by client implementation) + $data = $this->recursiveKeySort($data); // Build signature payload $payload = $timestamp . ':' . json_encode( @@ -109,6 +109,33 @@ final class ResponseSigner ]; } + /** + * Recursively sort array keys alphabetically + * + * @param mixed $data The data to sort + * @return mixed The sorted data + */ + private function recursiveKeySort(mixed $data): mixed + { + if (!is_array($data)) { + return $data; + } + + // Check if array is associative (has string keys) + $isAssociative = array_keys($data) !== range(0, count($data) - 1); + + if ($isAssociative) { + ksort($data); + } + + // Recursively sort nested arrays + foreach ($data as $key => $value) { + $data[$key] = $this->recursiveKeySort($value); + } + + return $data; + } + /** * Derive a unique signing key for a license * diff --git a/src/Api/RestApiController.php b/src/Api/RestApiController.php index d9c3fe8..e914446 100644 --- a/src/Api/RestApiController.php +++ b/src/Api/RestApiController.php @@ -95,29 +95,152 @@ final class RestApiController /** * Get client IP address + * + * Security note: Only trust proxy headers when explicitly configured. + * Set WC_LICENSE_TRUSTED_PROXIES constant or configure trusted_proxies + * in wp-config.php to enable proxy header support. + * + * @return string Client IP address */ private function getClientIp(): string { - $headers = [ - 'HTTP_CF_CONNECTING_IP', // Cloudflare - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_REAL_IP', - 'REMOTE_ADDR', - ]; + // Get the direct connection IP first + $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; - foreach ($headers as $header) { - if (!empty($_SERVER[$header])) { - $ips = explode(',', $_SERVER[$header]); - $ip = trim($ips[0]); - if (filter_var($ip, FILTER_VALIDATE_IP)) { - return $ip; + // Only check proxy headers if we're behind a trusted proxy + if ($this->isTrustedProxy($remoteAddr)) { + // Check headers in order of trust preference + $headers = [ + 'HTTP_CF_CONNECTING_IP', // Cloudflare + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_REAL_IP', + ]; + + foreach ($headers as $header) { + if (!empty($_SERVER[$header])) { + $ips = explode(',', $_SERVER[$header]); + $ip = trim($ips[0]); + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return $ip; + } } } } + // Validate and return direct connection IP + if (filter_var($remoteAddr, FILTER_VALIDATE_IP)) { + return $remoteAddr; + } + return '0.0.0.0'; } + /** + * Check if the given IP is a trusted proxy + * + * @param string $ip The IP address to check + * @return bool Whether the IP is a trusted proxy + */ + private function isTrustedProxy(string $ip): bool + { + // Check if trusted proxies are configured + if (!defined('WC_LICENSE_TRUSTED_PROXIES')) { + return false; + } + + $trustedProxies = WC_LICENSE_TRUSTED_PROXIES; + + // Handle string constant (comma-separated list) + if (is_string($trustedProxies)) { + $trustedProxies = array_map('trim', explode(',', $trustedProxies)); + } + + if (!is_array($trustedProxies)) { + return false; + } + + // Check for special keywords + if (in_array('CLOUDFLARE', $trustedProxies, true)) { + // Cloudflare IP ranges (simplified - in production, fetch from Cloudflare API) + if ($this->isCloudflareIp($ip)) { + return true; + } + } + + // Check direct IP match or CIDR notation + foreach ($trustedProxies as $proxy) { + if ($proxy === $ip) { + return true; + } + + // Support CIDR notation + if (str_contains($proxy, '/') && $this->ipMatchesCidr($ip, $proxy)) { + return true; + } + } + + return false; + } + + /** + * Check if IP is in Cloudflare range + * + * @param string $ip The IP to check + * @return bool Whether IP belongs to Cloudflare + */ + private function isCloudflareIp(string $ip): bool + { + // Cloudflare IPv4 ranges (as of 2024) + $cloudflareRanges = [ + '173.245.48.0/20', + '103.21.244.0/22', + '103.22.200.0/22', + '103.31.4.0/22', + '141.101.64.0/18', + '108.162.192.0/18', + '190.93.240.0/20', + '188.114.96.0/20', + '197.234.240.0/22', + '198.41.128.0/17', + '162.158.0.0/15', + '104.16.0.0/13', + '104.24.0.0/14', + '172.64.0.0/13', + '131.0.72.0/22', + ]; + + foreach ($cloudflareRanges as $range) { + if ($this->ipMatchesCidr($ip, $range)) { + return true; + } + } + + return false; + } + + /** + * Check if an IP matches a CIDR range + * + * @param string $ip The IP to check + * @param string $cidr The CIDR range (e.g., "192.168.1.0/24") + * @return bool Whether the IP matches the CIDR range + */ + private function ipMatchesCidr(string $ip, string $cidr): bool + { + [$subnet, $bits] = explode('/', $cidr); + + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || + !filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return false; + } + + $ipLong = ip2long($ip); + $subnetLong = ip2long($subnet); + $mask = -1 << (32 - (int) $bits); + + return ($ipLong & $mask) === ($subnetLong & $mask); + } + /** * Register REST API routes */ diff --git a/src/Plugin.php b/src/Plugin.php index 398077e..95d91c8 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -98,6 +98,7 @@ final class Plugin $this->twig = new Environment($loader, [ 'cache' => WP_CONTENT_DIR . '/cache/wc-licensed-product/twig', 'auto_reload' => true, // Always check for template changes + 'autoescape' => 'html', // Explicitly enable HTML autoescape for XSS protection ]); // Add WordPress functions as Twig functions diff --git a/templates/admin/licenses.html.twig b/templates/admin/licenses.html.twig index 3e70640..38af54f 100644 --- a/templates/admin/licenses.html.twig +++ b/templates/admin/licenses.html.twig @@ -1,6 +1,6 @@

{{ __('Licenses') }}

- + {{ __('Export CSV') }} @@ -143,8 +143,8 @@ - - {{ item.license.status|capitalize }} + + {{ esc_html(item.license.status)|capitalize }}
diff --git a/wc-licensed-product.php b/wc-licensed-product.php index 101058c..745afed 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Licensed Product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Description: WooCommerce plugin to sell software products using license keys with domain-based validation. - * Version: 0.3.5 + * Version: 0.3.6 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.3.5'); +define('WC_LICENSED_PRODUCT_VERSION', '0.3.6'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));