diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml
index 97c3bec0db..8d5f730840 100644
--- a/.mokogitea/manifest.xml
+++ b/.mokogitea/manifest.xml
@@ -4,7 +4,7 @@
MokoGitea
MokoConsulting
Moko fork of Gitea - adding project board REST API endpoints and custom enhancements
- 06.13.02
+ 06.13.03
v1.26.1+MOKO
GNU General Public License v3
diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index cb47eab79e..5da02b0051 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation
-# VERSION: 06.13.02
+# VERSION: 06.13.03
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/models/repo/repo_manifest.go b/models/repo/repo_manifest.go
index 989342530c..48dde42b93 100644
--- a/models/repo/repo_manifest.go
+++ b/models/repo/repo_manifest.go
@@ -5,6 +5,7 @@ package repo
import (
"context"
+ "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
@@ -60,10 +61,11 @@ func (RepoManifest) TableName() string {
}
// joomlaTypePrefix maps Joomla extension types to their element name prefixes.
+// Plugins have no prefix in Joomla's #__extensions table — the element is just
+// the lowercased name, and the folder column determines the plugin group.
var joomlaTypePrefix = map[string]string{
"component": "com_",
"module": "mod_",
- "plugin": "plg_",
"package": "pkg_",
"template": "tpl_",
"library": "lib_",
@@ -71,14 +73,17 @@ var joomlaTypePrefix = map[string]string{
}
// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokowaas).
+// The name is lowercased and hyphens are stripped to match the convention Joomla
+// uses in #__extensions.element (e.g. "MokoSuiteBackup" → "pkg_mokosuitebackup").
func (m *RepoManifest) AutoElementName() string {
if m.Name == "" || m.PackageType == "" {
return ""
}
+ lower := strings.ToLower(strings.ReplaceAll(m.Name, "-", ""))
if prefix, ok := joomlaTypePrefix[m.PackageType]; ok {
- return prefix + m.Name
+ return prefix + lower
}
- return m.Name
+ return lower
}
// FullElementName returns the effective element name: override if set, otherwise auto-constructed.
diff --git a/models/updateserver/license_key.go b/models/updateserver/license_key.go
index 3eac63278a..b3d1b6fa88 100644
--- a/models/updateserver/license_key.go
+++ b/models/updateserver/license_key.go
@@ -256,74 +256,8 @@ func ValidateLicenseKey(ctx context.Context, rawKey, domain string) (*LicenseKey
// Domain restriction check — skip for internal/master keys.
if domain != "" && !key.IsInternal {
- now := timeutil.TimeStampNow()
-
- if key.DomainRestriction != "" {
- // Domain restriction is set — enforce it.
- allowed := false
- for _, d := range strings.Split(key.DomainRestriction, ",") {
- if strings.EqualFold(strings.TrimSpace(d), domain) {
- allowed = true
- break
- }
- }
- if !allowed {
- // Check if still within the domain lock grace period.
- lockHours := pkg.DomainLockHours
- if lockHours > 0 && key.FirstUsedUnix > 0 {
- lockDeadline := key.FirstUsedUnix + timeutil.TimeStamp(int64(lockHours)*3600)
- if now < lockDeadline {
- // Grace period active — allow and auto-add this domain.
- _ = updateDomainRestriction(ctx, key.ID, domain)
- key.DomainRestriction = key.DomainRestriction + "," + domain
- allowed = true
- }
- }
- if !allowed {
- return nil, nil, fmt.Errorf("domain not allowed for this license key")
- }
- }
- } else {
- // No domain restriction set — auto-associate domain.
- maxSites := key.MaxSites
- if maxSites == 0 {
- maxSites = pkg.MaxSites
- }
- domainKnown, _ := IsDomainKnownForKey(ctx, key.ID, domain)
- if !domainKnown {
- if maxSites > 0 {
- uniqueDomains, err := CountUniqueDomainsByKey(ctx, key.ID)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to count domains: %w", err)
- }
- if uniqueDomains >= int64(maxSites) {
- return nil, nil, fmt.Errorf("site limit reached (%d/%d)", uniqueDomains, maxSites)
- }
- }
- _ = updateDomainRestriction(ctx, key.ID, domain)
- if key.DomainRestriction == "" {
- key.DomainRestriction = domain
- } else {
- key.DomainRestriction = key.DomainRestriction + "," + domain
- }
- }
- }
-
- // Site limit check: use key's MaxSites, fall back to package default.
- maxSites := key.MaxSites
- if maxSites == 0 {
- maxSites = pkg.MaxSites
- }
- if maxSites > 0 {
- uniqueDomains, err := CountUniqueDomainsByKey(ctx, key.ID)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to count domains: %w", err)
- }
- // Allow if this domain is already recorded, or if under the limit.
- domainKnown, _ := IsDomainKnownForKey(ctx, key.ID, domain)
- if !domainKnown && uniqueDomains >= int64(maxSites) {
- return nil, nil, fmt.Errorf("site limit reached (%d/%d)", uniqueDomains, maxSites)
- }
+ if err := validateAndAssociateDomain(ctx, key, pkg, domain); err != nil {
+ return nil, nil, err
}
}
@@ -374,6 +308,73 @@ func ValidateLicenseKeyForRepo(ctx context.Context, rawKey, domain string, repoI
return key, pkg, nil
}
+// validateAndAssociateDomain checks domain restrictions and auto-associates new
+// domains within a transaction to prevent TOCTOU races on the MaxSites limit.
+func validateAndAssociateDomain(ctx context.Context, key *LicenseKey, pkg *LicensePackage, domain string) error {
+ if key.DomainRestriction != "" {
+ // Domain restriction is set — enforce it.
+ allowed := false
+ for _, d := range strings.Split(key.DomainRestriction, ",") {
+ if strings.EqualFold(strings.TrimSpace(d), domain) {
+ allowed = true
+ break
+ }
+ }
+ if !allowed {
+ // Check if still within the domain lock grace period.
+ now := timeutil.TimeStampNow()
+ lockHours := pkg.DomainLockHours
+ if lockHours > 0 && key.FirstUsedUnix > 0 {
+ lockDeadline := key.FirstUsedUnix + timeutil.TimeStamp(int64(lockHours)*3600)
+ if now < lockDeadline {
+ // Grace period active — allow and auto-add this domain.
+ _ = updateDomainRestriction(ctx, key.ID, domain)
+ key.DomainRestriction = key.DomainRestriction + "," + domain
+ allowed = true
+ }
+ }
+ if !allowed {
+ return fmt.Errorf("domain not allowed for this license key")
+ }
+ }
+ return nil
+ }
+
+ // No domain restriction set — auto-associate domain within a transaction
+ // so the count check and insert are atomic (prevents exceeding MaxSites).
+ maxSites := key.MaxSites
+ if maxSites == 0 {
+ maxSites = pkg.MaxSites
+ }
+
+ return db.WithTx(ctx, func(txCtx context.Context) error {
+ domainKnown, _ := IsDomainKnownForKey(txCtx, key.ID, domain)
+ if domainKnown {
+ return nil // already associated, nothing to do
+ }
+
+ if maxSites > 0 {
+ uniqueDomains, err := CountUniqueDomainsByKey(txCtx, key.ID)
+ if err != nil {
+ return fmt.Errorf("failed to count domains: %w", err)
+ }
+ if uniqueDomains >= int64(maxSites) {
+ return fmt.Errorf("site limit reached (%d/%d)", uniqueDomains, maxSites)
+ }
+ }
+
+ if err := updateDomainRestriction(txCtx, key.ID, domain); err != nil {
+ return err
+ }
+ if key.DomainRestriction == "" {
+ key.DomainRestriction = domain
+ } else {
+ key.DomainRestriction = key.DomainRestriction + "," + domain
+ }
+ return nil
+ })
+}
+
// updateDomainRestriction appends a domain to a key's DomainRestriction field in the DB.
func updateDomainRestriction(ctx context.Context, keyID int64, domain string) error {
key, err := GetLicenseKeyByID(ctx, keyID)
diff --git a/routers/web/repo/changelog_xml.go b/routers/web/repo/changelog_xml.go
index db33a8a26d..c7d9c4117d 100644
--- a/routers/web/repo/changelog_xml.go
+++ b/routers/web/repo/changelog_xml.go
@@ -10,8 +10,9 @@ import (
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
- updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
+ updateserver_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/updateserver"
+ "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
@@ -57,15 +58,31 @@ func ServeChangelogXML(ctx *context.Context) {
return
}
- // Get extension metadata for element name and type.
- cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
+ // Resolve element name and type using the same priority as updates.xml:
+ // manifest first, then config table fallback, then repo-derived default.
element := strings.ToLower(repo.Name)
extType := "component"
+
+ manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
+ if err != nil {
+ log.Error("ServeChangelogXML: GetRepoManifest for repo %d: %v", repo.ID, err)
+ }
+ if manifest != nil {
+ if elem := manifest.FullElementName(); elem != "" {
+ element = elem
+ }
+ if manifest.PackageType != "" {
+ extType = manifest.PackageType
+ }
+ }
+
+ // Config table as fallback for repos without a manifest.
+ cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
if cfg != nil {
- if cfg.ExtensionName != "" {
+ if element == strings.ToLower(repo.Name) && cfg.ExtensionName != "" {
element = cfg.ExtensionName
}
- if cfg.ExtensionType != "" {
+ if extType == "component" && cfg.ExtensionType != "" {
extType = cfg.ExtensionType
}
}
diff --git a/routers/web/repo/updateserver.go b/routers/web/repo/updateserver.go
index 5ff4356a75..b55bc7fcec 100644
--- a/routers/web/repo/updateserver.go
+++ b/routers/web/repo/updateserver.go
@@ -78,9 +78,10 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool,
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
// from the repository's releases.
func ServeUpdatesXML(ctx *context.Context) {
- // Block if platform doesn't include joomla.
+ // Block if platform is set to a non-Joomla value.
+ // Empty/unset defaults to joomla for backwards compatibility.
platform := ctx.Data["RepoUpdatePlatform"]
- if platform == "dolibarr" {
+ if platform != nil && platform != "" && platform != "joomla" && platform != "both" {
ctx.NotFound(nil)
return
}