feat(licenses): full commercial license management system v1.26.1-moko.06.02.00 #402

Merged
jmiller merged 15 commits from dev into main 2026-06-02 12:00:24 +00:00

15 Commits

Author SHA1 Message Date
Jonathan Miller 0e7d3c4a34 fix(security): ownership guards, RepoScope parsing, CSRF tokens, XSS escaping
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
PR RC Release / Build RC Release (pull_request) Failing after 17s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
SECURITY: Add verifyPackageOwnership/verifyKeyOwnership checks to
all API handlers that accept ID parameters. Prevents cross-org
access where an admin of org A could modify org B's license data.

FIX: RepoScope validation now properly parses JSON arrays using
json.Unmarshal instead of strings.Contains. The old approach matched
substrings (repo ID "2" matched inside "12"). Now uses typed int64
comparison.

FIX: Add {{$.CsrfTokenHtml}} to both delete confirmation modal
forms (package and key) in repo and org templates. Without CSRF
tokens, the form-fetch-action POST requests would be rejected.

FIX: HTML-escape release notes in WordPress changelog to prevent
XSS via malicious release note content reaching WP admin dashboards.

FIX: Parse AllowedChannels JSON format before comma-split fallback
to avoid garbage values from splitting JSON arrays by comma.

FIX: Add missing third return value (false) on error path in
validateUpdateKey to prevent compile error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:59:29 -05:00
Jonathan Miller 913e919fba docs: update CHANGELOG and wiki for v1.26.1-moko.06.02.00
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 19s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Add changelog entry for full license management system release.
Update wiki with WordPress feed status, feed visibility modes,
and revision 1.4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:47:09 -05:00
Jonathan Miller a20153a0e0 fix(licenses): no-download mode shows release notes but hides files
In "no-download" feed visibility mode, anonymous users can browse
the release listing page and read release notes, but the download
section (attachments, source archives) is replaced with a "Sign in
to download" message.

Only "hidden" mode redirects anonymous users to login entirely.
"no-download" keeps the page public — release notes, changelogs,
and version info are all visible. Just the actual files are gated.

This matches the commercial pattern: let users see what's available
to motivate purchase, while protecting the actual deliverables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:42:44 -05:00
Jonathan Miller a149edccd3 feat(licenses): feed visibility modes and login-required releases
Add FeedVisibility field to UpdateStreamConfig with three modes:
- public: full feed with download URLs (default)
- no-download: version info visible but download URLs stripped
- hidden: empty feed returned without a valid license key

The "no-download" mode is the key commercial pattern — customers
see updates exist (motivating purchase/renewal) but cannot download
without a valid key. Joomla shows "update available" in admin.

Applied consistently across all update feed endpoints (Joomla XML,
Dolibarr JSON, WordPress JSON) via the shared validateUpdateKey()
which now returns a stripDownloads flag.

Also: when licensing is enabled, the release listing page requires
login. Anonymous users are redirected to the login page. This
prevents browsing release notes and download links without auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:38:09 -05:00
Jonathan Miller 01eb9944ca feat(licenses): replace channel checkboxes with combolist picker
Use the existing shared/combolist.tmpl component for channel
selection in create and edit package forms. This mimics the
issue label picker UI — a searchable dropdown with checkmarks
and a selected-items list below.

Replaces raw checkboxes (repo + org templates) and Fomantic
multiple selection dropdowns (edit templates) with the
combo-multiselect component that has proper JS for toggle,
search, and clear functionality.

Handler parsing updated: combolist sends comma-separated values
in a single hidden input (vs multiple checkbox form values).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:30:59 -05:00
Jonathan Miller e3a8ae2595 feat(settings): add extension metadata to repo settings (#335)
Repo settings now include extension metadata fields (element name,
display name, type, target version, maintainer, PHP minimum) under
the Licensing section. These override org-level defaults per-repo.

Empty fields inherit from the organization's update stream config.
Extension type dropdown includes: package, component, module,
plugin, template, library — plus an "(inherit from org)" option.

Also adds form fields to the RepoSettingsForm struct for all
metadata fields.

Closes #335

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 06:19:48 -05:00
Jonathan Miller 1fabdb94ec feat(updates): WordPress PUC-compatible update feed (#351)
New endpoint: GET /{owner}/{repo}/updates/wordpress.json

Generates JSON compatible with the YahnisElsts plugin-update-checker
library — the standard for commercial WordPress plugin self-hosted
updates. Returns name, slug, version, download_url, homepage,
requires_php, author, sections (changelog HTML), icons, and banners.

License key validation: reads from ?license_key=, ?dlid=, or ?key=
query params (PUC sends these via addQueryArgFilter). When RequireKey
is enabled, returns minimal empty response without download_url.

Changelog section built from release notes (last 10 stable releases),
converting markdown list items to HTML <ul>/<li> elements.

Icon/banner URLs point to conventional paths in the repo:
  assets/icon-128x128.png, assets/icon-256x256.png
  assets/banner-772x250.png, assets/banner-1544x500.png

Route registered at /updates/wordpress.json alongside existing
/updates.xml (Joomla) and /updates/dolibarr.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 00:01:16 -05:00
Jonathan Miller d204ecb9f0 fix(licenses): enforce RepoScope validation and add API revoke endpoint
SECURITY: ValidateLicenseKeyForRepo() now checks the package's
RepoScope field against the requesting repo ID. A package scoped
to repo A will reject keys when accessed from repo B's update feed.
Update server and download gating both use the new function.

Master/internal keys bypass repo scope checks.

RepoScope supports: "all" (any repo), single repo ID string,
or JSON array of repo IDs like ["1","5","12"].

Also adds POST /license-keys/{id}/revoke API endpoint that was
missing from the API but existed in web handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:33:26 -05:00
Jonathan Miller 93d18ab25f feat(licenses): double confirmation modals for permanent deletion (#391)
Replace link-action delete buttons with show-modal pattern that opens
a Fomantic UI modal with a form. Package deletion requires typing
the package name in a required field before the submit button works.
Key deletion shows a warning modal with confirmation.

Uses Gitea's existing form-fetch-action + modal_actions_confirm
pattern, matching how repo deletion works in settings.

Both repo and org templates updated with matching modals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:23:56 -05:00
Jonathan Miller 3e8124a2b7 feat(licenses): API package CRUD, settings API, and repo scope UI
API package endpoints (#388):
- PATCH /license-packages/{id} — edit package (master protected)
- DELETE /license-packages/{id} — delete package (master protected)
- POST /license-packages/{id}/archive — archive package
- POST /license-packages/{id}/unarchive — restore package

Settings API (#349):
- GET /license-settings — read licensing/update stream config
- PUT /license-settings — update config (all metadata fields)
- New LicenseSettings struct in API types

Repo scope UI (#395):
- Dropdown in create/edit package forms listing org repos
- "All repositories" default option
- RepoScope read from form in both repo and org handlers
- OrgRepos loaded via GetOrgRepositories in Licenses handlers

Refs #341, #346, #349, #388, #395

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:21:09 -05:00
Jonathan Miller 7c9215de05 test(licenses): add integration tests for license key API endpoints
Tests cover:
- Package CRUD (list, create, create without name)
- Key CRUD (create, list, edit, renew, delete)
- Purchase webhook (new key, idempotent duplicate)
- Public validation (valid key, invalid key, domain check)

Tests follow existing Gitea integration test patterns using
unittest fixtures, user tokens, and DecodeJSON assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:09:14 -05:00
Jonathan Miller 7da0e025da feat(updates): include SHA256 from sidecar files in Joomla updates.xml
Read the .sha256 sidecar attachment (generated by
GenerateReleaseChecksums) and populate the <sha256> element in the
update XML. This matches the pattern used by Akeeba (sha512) and
JCE (sha256 + sha384 + sha512) for integrity verification.

Also fix zip attachment filter to skip .sha256 sidecar files when
selecting the download URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:04:24 -05:00
Jonathan Miller 0e09723d2a fix(updates): map stream names to Joomla-standard tag values
Joomla only recognizes: dev, alpha, beta, rc, stable. Our internal
stream names use longer forms (development, release-candidate).
Add joomlaTagName() to map between conventions in the <tags><tag>
XML element.

Without this, Joomla's update channel filter ignores entries with
non-standard tag values like "release-candidate" or "development".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:01:59 -05:00
Jonathan Miller 53a5d0b97b feat(licenses): domain lock timer, infourl fix, Akeeba-compatible XML format
Domain lock timer: add DomainLockHours to LicensePackage and
FirstUsedUnix to LicenseKey. During the grace period after first
use, any domain is accepted and auto-added to the restriction list.
After the grace period, only listed domains are allowed. Set 0 for
immediate lock-on-first-use (default).

Fix infourl: default to /releases listing page instead of specific
tag page. Falls back to SupportURL or InfoURL if configured.

Match Akeeba Backup Pro XML format: downloadkey prefix is "dlid="
(not "&dlid="), matching how Joomla stores extra_query. Verified
against production Akeeba/JCE/AdminTools manifests via SSH.

Update migration v340 with FirstUsedUnix and DomainLockHours columns.

Add DomainLockHours field to create/edit package forms for both
repo and org levels with help text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 05:00:50 -05:00
Jonathan Miller 448b7d3ab0 feat(licenses): archive, search, download gating, changelog XML, and expanded permissions
Migration v340: sync all missing columns (key_raw, payment_ref,
last_heartbeat_unix, is_archived, licensing_enabled, download_gating,
support_url, and all extension metadata fields).

Package archiving (#384): add IsArchived field with archive/unarchive
handlers and collapsible "Archived Packages" section in templates.
Existing keys from archived packages continue to work.

Expanded delete permissions (#385): org owners and site admins can
permanently delete packages and keys (previously site admin only).

Search (#392): server-side search across key_prefix, key_raw,
licensee_name, licensee_email, domain_restriction, and payment_ref
via ?q= query parameter on both repo and org licenses pages.

Sortable tables (#390): Fomantic UI sortable class on keys table
with new Domain column showing DomainRestriction per key.

Download gating (#347): three modes — none, prerelease-only, and
all downloads. CheckDownloadGating() intercepts both release
attachment and git archive download handlers.

Support URL (#393): configurable SupportURL field on
UpdateStreamConfig for wiki or external site links.

Changelog XML (#343): ServeChangelogXML endpoint at /changelog.xml
generates Joomla-compatible changelog from release notes. Parses
Keep-a-Changelog markdown sections into <security>, <fix>,
<addition>, <change>, <remove>, <note> XML elements.

API renew (#387): POST /license-keys/{id}/renew endpoint extends
key expiration by package duration.

Closes #384, #385, #386, #387, #389, #390, #392, #393
Refs #343, #346, #347

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-01 04:45:20 -05:00