Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bf7313e40 | |||
| 6c06384966 |
@@ -405,11 +405,8 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
|
||||
perm.units = repo.Units
|
||||
|
||||
// anonymous user visit private repo.
|
||||
// Still process unit-level anonymous access so that units with
|
||||
// AnonymousAccessMode (e.g. public wiki on a private repo) are visible.
|
||||
if user == nil && repo.IsPrivate {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -673,14 +673,6 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||
}
|
||||
|
||||
// Include private repos that have at least one unit with public anonymous access.
|
||||
// This enables discovery of repos where e.g. wiki or releases are public.
|
||||
cond = cond.Or(builder.In("`repository`.id",
|
||||
builder.Select("repo_id").From("repo_unit").Where(
|
||||
builder.Gt{"anonymous_access_mode": 0},
|
||||
),
|
||||
))
|
||||
|
||||
if user != nil {
|
||||
// 2. Be able to see all repositories that we have unit independent access to
|
||||
// 3. Be able to see all repositories through team membership(s)
|
||||
|
||||
@@ -128,15 +128,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
}
|
||||
|
||||
// Only public pull don't need auth.
|
||||
// For private repos, also allow anonymous pull if the specific unit
|
||||
// (code or wiki) has AnonymousAccessMode >= Read.
|
||||
isPublicPull := repoExist && isPull && !repo.IsPrivate
|
||||
if repoExist && isPull && repo.IsPrivate {
|
||||
repoUnit := repo.MustGetUnit(ctx, unitType)
|
||||
if repoUnit.AnonymousAccessMode >= perm.AccessModeRead {
|
||||
isPublicPull = true
|
||||
}
|
||||
}
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
|
||||
// don't allow anonymous pulls if organization is not public
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/updateserver"
|
||||
)
|
||||
|
||||
// ServeUpdatesXML generates and serves a Joomla-compatible updates.xml
|
||||
// from the repository's releases.
|
||||
func ServeUpdatesXML(ctx *context.Context) {
|
||||
xmlData, err := updateserver.GenerateJoomlaXML(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GenerateJoomlaXML", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(xmlData)
|
||||
}
|
||||
@@ -1494,6 +1494,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
|
||||
// end "/{username}/{reponame}": repo releases
|
||||
|
||||
// "/{username}/{reponame}": update server (Joomla-compatible updates.xml)
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/updates.xml", repo.ServeUpdatesXML)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package updateserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
|
||||
// Joomla-compatible updates.xml structures for XML marshaling.
|
||||
|
||||
type xmlUpdates struct {
|
||||
XMLName xml.Name `xml:"updates"`
|
||||
Updates []xmlUpdate `xml:"update"`
|
||||
}
|
||||
|
||||
type xmlUpdate struct {
|
||||
Name string `xml:"name"`
|
||||
Description string `xml:"description"`
|
||||
Element string `xml:"element"`
|
||||
Type string `xml:"type"`
|
||||
Client string `xml:"client"`
|
||||
Version string `xml:"version"`
|
||||
CreationDate string `xml:"creationDate"`
|
||||
InfoURL xmlInfoURL `xml:"infourl"`
|
||||
Downloads xmlDownloads `xml:"downloads"`
|
||||
SHA256 string `xml:"sha256,omitempty"`
|
||||
Tags xmlTags `xml:"tags"`
|
||||
ChangelogURL string `xml:"changelogurl,omitempty"`
|
||||
Maintainer string `xml:"maintainer,omitempty"`
|
||||
MaintainerURL string `xml:"maintainerurl,omitempty"`
|
||||
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
|
||||
}
|
||||
|
||||
type xmlInfoURL struct {
|
||||
Title string `xml:"title,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlDownloads struct {
|
||||
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
|
||||
}
|
||||
|
||||
type xmlDownloadURL struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Format string `xml:"format,attr"`
|
||||
URL string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type xmlTags struct {
|
||||
Tag string `xml:"tag"`
|
||||
}
|
||||
|
||||
type xmlTargetPlat struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
func channelFromTag(tagName string, isPrerelease bool) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
switch {
|
||||
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
|
||||
return "dev"
|
||||
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
|
||||
return "alpha"
|
||||
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
|
||||
return "beta"
|
||||
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
|
||||
return "rc"
|
||||
case isPrerelease:
|
||||
return "rc"
|
||||
default:
|
||||
return "stable"
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
|
||||
// It returns the raw XML bytes. The element, maintainer, and target platform
|
||||
// are derived from the repo name and owner.
|
||||
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository) ([]byte, error) {
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IncludeDrafts: false,
|
||||
IncludeTags: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
baseURL := setting.AppURL
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
||||
|
||||
element := strings.ToLower(repo.Name)
|
||||
|
||||
// Track best (latest) release per channel to emit one entry per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
var updates xmlUpdates
|
||||
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Load attachments for download URLs.
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the first .zip attachment as the download URL.
|
||||
var downloadURL string
|
||||
for _, att := range rel.Attachments {
|
||||
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
||||
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to the release tag archive if no zip attachment.
|
||||
if downloadURL == "" {
|
||||
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := channelSuffix(ch)
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
|
||||
u := xmlUpdate{
|
||||
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
|
||||
Element: element,
|
||||
Type: "component",
|
||||
Client: "site",
|
||||
Version: version,
|
||||
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
||||
InfoURL: xmlInfoURL{
|
||||
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
||||
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
||||
},
|
||||
Downloads: xmlDownloads{
|
||||
DownloadURL: []xmlDownloadURL{
|
||||
{Type: "full", Format: "zip", URL: downloadURL},
|
||||
},
|
||||
},
|
||||
Tags: xmlTags{Tag: ch},
|
||||
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
||||
Maintainer: repo.Owner.Name,
|
||||
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
|
||||
TargetPlatform: xmlTargetPlat{
|
||||
Name: "joomla",
|
||||
Version: ".*",
|
||||
},
|
||||
}
|
||||
|
||||
updates.Updates = append(updates.Updates, u)
|
||||
}
|
||||
|
||||
output, err := xml.MarshalIndent(updates, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), output...), nil
|
||||
}
|
||||
|
||||
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
|
||||
func extractVersion(tagName string) string {
|
||||
v := tagName
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
v = strings.TrimPrefix(v, "release-")
|
||||
v = strings.TrimPrefix(v, "release/")
|
||||
// Strip channel suffixes to get base version.
|
||||
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
|
||||
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
|
||||
v = v[:idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// channelSuffix returns the version suffix for a channel.
|
||||
func channelSuffix(channel string) string {
|
||||
switch channel {
|
||||
case "dev":
|
||||
return "-dev"
|
||||
case "alpha":
|
||||
return "-alpha"
|
||||
case "beta":
|
||||
return "-beta"
|
||||
case "rc":
|
||||
return "-rc"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 05.14.00
|
||||
VERSION: 05.05.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -87,13 +87,13 @@
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.14.00</version>
|
||||
<creationDate>2026-05-31</creationDate>
|
||||
<version>05.05.00</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.14.00.zip</downloadurl>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>bec4bf5a1a841f8e72d9826451004db5d8afc70144231dfedc7fb01a6695955c</sha256>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user