Compare commits
30 Commits
rc
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfcc9f79a | |||
| e427bee210 | |||
| 3af6265802 | |||
| c456258540 | |||
| 91e50fe4bf | |||
| 9b766c3dee | |||
| 7ca3593f74 | |||
| bf5b759398 | |||
| d877bbbe0c | |||
| 5e401c659b | |||
| 7d0b3f042c | |||
| e0c492c7df | |||
| 5fec351426 | |||
| 7f09fa0b98 | |||
| 1b0b62db38 | |||
| 0d9d526203 | |||
| b321273306 | |||
| 8adccbcb40 | |||
| 08f6454dd2 | |||
| a83d2ee3bd | |||
| 73a1320d72 | |||
| 4694fbf719 | |||
| 0a0d7b704d | |||
| 3cc68ec310 | |||
| 96a51574e2 | |||
| 93f20b9671 | |||
| d419033384 | |||
| c453310834 | |||
| 041adc50e5 | |||
| cd305a2332 |
@@ -4,7 +4,7 @@
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>06.15.00</version>
|
||||
<version>06.16.01</version>
|
||||
<version-prefix>v1.26.1+MOKO</version-prefix>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
|
||||
@@ -13,13 +13,6 @@
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
- version/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 06.15.00
|
||||
# VERSION: 06.16.01
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -8,4 +8,245 @@
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- 'fix/**'
|
||||
- 'patch/**'
|
||||
- 'hotfix/**'
|
||||
- 'bugfix/**'
|
||||
- 'chore/**'
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Auto-detect and update platform if not set in manifest
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
case "${{ github.ref_name }}" in
|
||||
rc) STABILITY="release-candidate" ;;
|
||||
alpha) STABILITY="alpha" ;;
|
||||
beta) STABILITY="beta" ;;
|
||||
*) STABILITY="development" ;;
|
||||
esac
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# Read back element outputs
|
||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -33,7 +33,8 @@ on:
|
||||
- scripts
|
||||
- repo
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
+8
-56
@@ -1,61 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [06.15.00] --- 2026-06-12
|
||||
## [06.16.00] --- 2026-06-18
|
||||
|
||||
## [06.15.00] --- 2026-06-18
|
||||
|
||||
## [06.15.00] --- 2026-06-18
|
||||
|
||||
|
||||
## [06.14.00] --- 2026-06-14
|
||||
|
||||
## [06.15.00] --- 2026-06-12
|
||||
|
||||
* FEATURES
|
||||
* feat(custom-fields): required flag UI and API validation (#597, PR #612)
|
||||
* Required checkbox in org custom field settings
|
||||
* Red asterisk indicator on required fields
|
||||
* API returns 422 when required custom fields are missing
|
||||
* Validation runs before issue creation (no orphaned issues)
|
||||
* feat(issues): make status_id, priority_id, type_id required on issue create (#598, PR #613)
|
||||
* `CreateIssueOption` fields changed from optional `*int64` to `int64`
|
||||
* Auto-assigns org defaults when value is 0
|
||||
* MCP `gitea_issue_create` now requires these fields (pass 0 for defaults)
|
||||
* Explicit metadata errors now return 500 instead of being silently discarded
|
||||
|
||||
## [06.14.00] --- 2026-06-11
|
||||
|
||||
* FIXES
|
||||
* fix: return 404 for update feeds when update server is disabled (#589, PR #599)
|
||||
* fix(ui): raw file button opens in new tab with rel="noopener noreferrer" (#581, PR #600)
|
||||
* fix: update server feed generation bugs (#601, PR #605)
|
||||
* default targetplatform changed from `(5|6)\\..*` to `6\\..*` for Joomla 6 compat
|
||||
* `<client>` uses string values `site`/`administrator` per Joomla update spec (#611)
|
||||
* pre-release version suffix number preserved (e.g. `-rc2` not `-rc`)
|
||||
* feed generator uses `FullElementName()` for auto-constructed element names
|
||||
* fix: wiki API sub-page support and content response (#606, #607, PR #608)
|
||||
* wiki routes use wildcard to support pages with path separators
|
||||
* `ListWikiPages` returns pages in subdirectories
|
||||
* error logging for empty content_base64 responses
|
||||
* fix: deploy workflow clones wrong repo and runs swapoff (#609)
|
||||
* removed `swapoff -a` that crashed MySQL during deploys
|
||||
* fixed source repo URL from MokoGitea to MokoGitea-APP
|
||||
|
||||
* MCP
|
||||
* metadata update tool now exposes element_name, display_name, description, license_name, language fields
|
||||
|
||||
## [06.14.00] --- 2026-06-11
|
||||
|
||||
|
||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||
|
||||
## [06.14.00] --- 2026-06-09
|
||||
|
||||
* FEATURES
|
||||
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
|
||||
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
|
||||
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
|
||||
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
|
||||
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
|
||||
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
|
||||
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
|
||||
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
|
||||
|
||||
* MIGRATIONS
|
||||
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
|
||||
|
||||
Submodule
+1
Submodule mcp-mokogitea-api added at dbaf91546e
@@ -5,6 +5,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
@@ -25,7 +26,6 @@ type RepoManifest struct {
|
||||
Name string `xorm:"TEXT 'name'"` // project name
|
||||
Org string `xorm:"TEXT 'org'"` // organization name
|
||||
Description string `xorm:"TEXT 'description'"` // project description
|
||||
Version string `xorm:"TEXT 'version'"` // current version string
|
||||
LicenseSPDX string `xorm:"VARCHAR(50) 'license_spdx'"` // SPDX identifier, e.g. "GPL-3.0-or-later"
|
||||
LicenseName string `xorm:"TEXT 'license_name'"` // human-readable license name
|
||||
|
||||
@@ -60,10 +60,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 the
|
||||
// lowercased, hyphen-free 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 +72,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 removed to produce clean element names
|
||||
// for the #__extensions.element column (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.
|
||||
|
||||
@@ -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,80 @@ func ValidateLicenseKeyForRepo(ctx context.Context, rawKey, domain string, repoI
|
||||
return key, pkg, nil
|
||||
}
|
||||
|
||||
// validateAndAssociateDomain checks domain restrictions and auto-associates new
|
||||
// domains. The auto-associate path (no existing restriction) runs inside a
|
||||
// transaction to prevent TOCTOU races on the MaxSites limit. The grace-period
|
||||
// path (existing restriction, lock window open) also propagates DB errors.
|
||||
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.
|
||||
if err := updateDomainRestriction(ctx, key.ID, domain); err != nil {
|
||||
return fmt.Errorf("failed to auto-add domain during grace period: %w", err)
|
||||
}
|
||||
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, err := IsDomainKnownForKey(txCtx, key.ID, domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check domain association: %w", err)
|
||||
}
|
||||
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 fmt.Errorf("failed to update domain restriction: %w", 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)
|
||||
|
||||
@@ -16,7 +16,6 @@ type apiManifest struct {
|
||||
Name string `json:"name"`
|
||||
Org string `json:"org"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
LicenseSPDX string `json:"license_spdx"`
|
||||
LicenseName string `json:"license_name"`
|
||||
VersionPrefix string `json:"version_prefix"`
|
||||
@@ -65,7 +64,7 @@ func GetRepoManifest(ctx *context.APIContext) {
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
Version: m.Version,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
@@ -108,7 +107,7 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Name: req.Name,
|
||||
Org: req.Org,
|
||||
Description: req.Description,
|
||||
Version: req.Version,
|
||||
|
||||
LicenseSPDX: req.LicenseSPDX,
|
||||
LicenseName: req.LicenseName,
|
||||
VersionPrefix: req.VersionPrefix,
|
||||
@@ -136,7 +135,7 @@ func UpdateRepoManifest(ctx *context.APIContext) {
|
||||
Name: m.Name,
|
||||
Org: m.Org,
|
||||
Description: m.Description,
|
||||
Version: m.Version,
|
||||
|
||||
LicenseSPDX: m.LicenseSPDX,
|
||||
LicenseName: m.LicenseName,
|
||||
VersionPrefix: m.VersionPrefix,
|
||||
|
||||
@@ -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,35 @@ 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:
|
||||
// manifest first, then config table fallback, then repo-derived default.
|
||||
element := strings.ToLower(repo.Name)
|
||||
extType := "component"
|
||||
elementFromManifest := false
|
||||
extTypeFromManifest := false
|
||||
|
||||
manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
|
||||
if err != nil {
|
||||
log.Warn("ServeChangelogXML: GetRepoManifest for repo %d: %v", repo.ID, err)
|
||||
}
|
||||
if manifest != nil {
|
||||
if elem := manifest.FullElementName(); elem != "" {
|
||||
element = elem
|
||||
elementFromManifest = true
|
||||
}
|
||||
if manifest.PackageType != "" {
|
||||
extType = manifest.PackageType
|
||||
extTypeFromManifest = true
|
||||
}
|
||||
}
|
||||
|
||||
// Config table fallback: apply only when the manifest did not provide a value.
|
||||
cfg := updateserver_model.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
|
||||
if cfg != nil {
|
||||
if cfg.ExtensionName != "" {
|
||||
if !elementFromManifest && cfg.ExtensionName != "" {
|
||||
element = cfg.ExtensionName
|
||||
}
|
||||
if cfg.ExtensionType != "" {
|
||||
if !extTypeFromManifest && cfg.ExtensionType != "" {
|
||||
extType = cfg.ExtensionType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
platform := ctx.Data["RepoUpdatePlatform"]
|
||||
if platform == "dolibarr" {
|
||||
// Block if platform is set to a non-Joomla value.
|
||||
// Empty/unset defaults to joomla for backwards compatibility.
|
||||
platform, _ := ctx.Data["RepoUpdatePlatform"].(string)
|
||||
if platform != "" && platform != "joomla" && platform != "both" {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -383,12 +383,22 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
|
||||
infoURL = meta.SupportURL
|
||||
}
|
||||
|
||||
// Joomla <client> element uses string values per the update server spec.
|
||||
// Joomla's XML parser maps these to client_id internally (0/1).
|
||||
client := "site"
|
||||
// Joomla <client> element must match the client_id stored in #__extensions.
|
||||
// Joomla's update finder matches by (element, type, client_id, folder) —
|
||||
// a mismatch causes extension_id=0 and the update never shows.
|
||||
//
|
||||
// Joomla hardcodes client_id per extension type in the installer adapters:
|
||||
// component → client_id=1 (ComponentAdapter.php:900)
|
||||
// package → client_id=0 (PackageAdapter.php:548)
|
||||
// plugin → client_id=0 (PluginAdapter.php:492)
|
||||
// library → client_id=0 (LibraryAdapter.php:420)
|
||||
// file → client_id=0 (FileAdapter.php:422)
|
||||
// module → client_id from manifest (0=site, 1=admin)
|
||||
// template → client_id from manifest (0=site, 1=admin)
|
||||
client := "site" // default: client_id=0
|
||||
switch extType {
|
||||
case "package", "component", "library", "file":
|
||||
client = "administrator"
|
||||
case "component":
|
||||
client = "administrator" // client_id=1
|
||||
}
|
||||
|
||||
u := xmlUpdate{
|
||||
|
||||
Reference in New Issue
Block a user