diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 85229994b4..78c13f33a0 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -72,6 +72,8 @@ var migrations = []*Migration{ NewMigration("Create the `federated_user` table", CreateFederatedUserTable), // v17 -> v18 NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser), + // v18 -> v19 + NewMigration("Create the `following_repo` table", CreateFollowingRepoTable), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v18.go b/models/forgejo_migrations/v18.go new file mode 100644 index 0000000000..afccfbfe15 --- /dev/null +++ b/models/forgejo_migrations/v18.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import "xorm.io/xorm" + +type FollowingRepo struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"` + ExternalID string `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"` + FederationHostID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"` + URI string +} + +func CreateFollowingRepoTable(x *xorm.Engine) error { + return x.Sync(new(FederatedUser)) +} diff --git a/modules/forgefed/actor.go b/modules/forgefed/actor.go index d3cae20dec..0ef46185d1 100644 --- a/modules/forgefed/actor.go +++ b/modules/forgefed/actor.go @@ -8,7 +8,6 @@ import ( "net/url" "strings" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/validation" ap "github.com/go-ap/activitypub" @@ -71,10 +70,6 @@ type PersonID struct { // Factory function for PersonID. Created struct is asserted to be valid func NewPersonID(uri, source string) (PersonID, error) { - // TODO: remove after test - //if !validation.IsValidExternalURL(uri) { - // return PersonId{}, fmt.Errorf("uri %s is not a valid external url", uri) - //} result, err := newActorID(uri) if err != nil { return PersonID{}, err @@ -126,16 +121,13 @@ type RepositoryID struct { // Factory function for RepositoryID. Created struct is asserted to be valid. func NewRepositoryID(uri, source string) (RepositoryID, error) { - if !validation.IsAPIURL(uri) { - return RepositoryID{}, fmt.Errorf("uri %s is not a valid repo url on this host %s", uri, setting.AppURL+"api") - } result, err := newActorID(uri) if err != nil { return RepositoryID{}, err } result.Source = source - // validate Person specific path + // validate Person specific repoID := RepositoryID{result} if valid, err := validation.IsValid(repoID); !valid { return RepositoryID{}, err diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 59eb11240d..4c20f875ba 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1131,7 +1131,6 @@ form.reach_limit_of_creation_1=Du hast bereits dein Limit von %d Repository erre form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositorys erreicht. form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_pattern_not_allowed=Das Muster „%s“ ist in Repository-Namen nicht erlaubt. -form.string_too_long=Der angegebene String ist länger als %d Zeichen. need_auth=Authentifizierung migrate_options=Migrationsoptionen @@ -2061,10 +2060,6 @@ settings.collaboration.undefined=Nicht definiert settings.hooks=Webhooks settings.githooks=Git-Hooks settings.basic_settings=Grundeinstellungen -settings.federation_settings=Föderationseinstellungen -settings.federation_apapiurl=Föderierungs-URL dieses Repositories. Kopiere sie und füge sie in die Föderationseinstellungen eines anderen Repository ein als dem Repository folgendes Repository. -settings.federation_following_repos=URLs der Repos, die diesem Repo folgen. Getrennt mittels ";", keine Leerzeichen. -settings.federation_not_enabled=Föderierung ist auf deiner Instanz nicht aktiviert. settings.mirror_settings=Spiegeleinstellungen settings.mirror_settings.docs=Richte dein Repository so ein, dass es automatisch Commits, Tags und Branches mit einem anderen Repository synchronisieren kann. settings.mirror_settings.docs.disabled_pull_mirror.instructions=Richte dein Projekt so ein, dass es automatisch Commits, Tags und Branches in ein anderes Repository pusht. Pull-Spiegel wurden von deinem Website-Administrator deaktiviert. diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index b29ab3c4a9..66e96b9961 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -391,22 +391,21 @@ func SettingsPost(ctx *context.Context) { ctx.Flash.Info(ctx.Tr("repo.settings.federation_not_enabled")) return } - // ToDo: Rename to followingRepos - federationRepos := strings.TrimSpace(form.FederationRepos) - federationRepos = strings.TrimSuffix(federationRepos, ";") + followingRepos := strings.TrimSpace(form.FollowingRepos) + followingRepos = strings.TrimSuffix(followingRepos, ";") maxFollowingRepoStrLength := 2048 - errs := validation.ValidateMaxLen(federationRepos, maxFollowingRepoStrLength, "federationRepos") + errs := validation.ValidateMaxLen(followingRepos, maxFollowingRepoStrLength, "federationRepos") if len(errs) > 0 { - ctx.Data["ERR_FederationRepos"] = true + ctx.Data["ERR_FollowingRepos"] = true ctx.Flash.Error(ctx.Tr("repo.form.string_too_long", maxFollowingRepoStrLength)) ctx.Redirect(repo.Link() + "/settings") return } federationRepoSplit := []string{} - if federationRepos != "" { - federationRepoSplit = strings.Split(federationRepos, ";") + if followingRepos != "" { + federationRepoSplit = strings.Split(followingRepos, ";") } for idx, repo := range federationRepoSplit { federationRepoSplit[idx] = strings.TrimSpace(repo) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1bc06b1b9a..e826d179ed 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -114,7 +114,7 @@ type RepoSettingForm struct { RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` Description string `binding:"MaxSize(2048)"` Website string `binding:"ValidUrl;MaxSize(1024)"` - FederationRepos string + FollowingRepos string Interval string MirrorAddress string MirrorUsername string diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index de86cba77a..584c1024de 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -6,9 +6,11 @@ package integration import ( "fmt" "net/http" + "net/http/httptest" "testing" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/forgefed" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" @@ -263,3 +265,59 @@ func TestProtectedBranch(t *testing.T) { unittest.AssertCount(t, &git_model.ProtectedBranch{RuleName: "master", RepoID: repo.ID}, 1) }) } + +func TestRepoFollowing(t *testing.T) { + setting.Federation.Enabled = true + defer tests.PrepareTestEnv(t)() + defer func() { + setting.Federation.Enabled = false + }() + + federatedRoutes := http.NewServeMux() + federatedRoutes.HandleFunc("/.well-known/nodeinfo", + func(res http.ResponseWriter, req *http.Request) { + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo + responseBody := fmt.Sprintf(`{"links":[{"href":"http://%s/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`, req.Host) + t.Logf("response: %s", responseBody) + // TODO: as soon as content-type will become important: content-type: application/json;charset=utf-8 + fmt.Fprint(res, responseBody) + }) + federatedRoutes.HandleFunc("/api/v1/nodeinfo", + func(res http.ResponseWriter, req *http.Request) { + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/nodeinfo + responseBody := fmt.Sprintf(`{"version":"2.1","software":{"name":"forgejo","version":"1.20.0+dev-3183-g976d79044",` + + `"repository":"https://codeberg.org/forgejo/forgejo.git","homepage":"https://forgejo.org/"},` + + `"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},` + + `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) + fmt.Fprint(res, responseBody) + }) + federatedRoutes.HandleFunc("/", + func(res http.ResponseWriter, req *http.Request) { + t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) + }) + federatedSrv := httptest.NewServer(federatedRoutes) + defer federatedSrv.Close() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID}) + session := loginUser(t, user.Name) + + t.Run("Add a following repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + link := fmt.Sprintf("/%s/settings", repo.FullName()) + + req := NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": GetCSRF(t, session, link), + "action": "federation", + "following_repos": fmt.Sprintf("%s/api/v1/activitypub/repository-id/1", federatedSrv.URL), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Verify it was added. + federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"}) + unittest.AssertExistsAndLoadBean(t, &repo_model.FollowingRepo{ + ExternalID: "1", + FederationHostID: federationHost.ID, + }) + }) +}