diff --git a/models/repo/following_repo.go b/models/repo/following_repo.go index a935433e40..1fb90406f6 100644 --- a/models/repo/following_repo.go +++ b/models/repo/following_repo.go @@ -14,14 +14,18 @@ type FollowingRepo struct { RepoID int64 `xorm:"NOT NULL"` ExternalID string `xorm:"TEXT UNIQUE(federation_repo_mapping) NOT NULL"` FederationHostID int64 `xorm:"UNIQUE(federation_repo_mapping) NOT NULL"` + ExternalOwner string `xorm:"TEXT UNIQUE(federation_repo_mapping) NOT NULL"` + ExternalRepoName string `xorm:"TEXT UNIQUE(federation_repo_mapping) NOT NULL"` Uri string } -func NewFollowingRepo(repoID int64, externalID string, federationHostID int64, uri string) (FollowingRepo, error) { +func NewFollowingRepo(repoID int64, externalID string, federationHostID int64, externalOwner string, externalRepoName string, uri string) (FollowingRepo, error) { result := FollowingRepo{ RepoID: repoID, ExternalID: externalID, FederationHostID: federationHostID, + ExternalOwner: externalOwner, + ExternalRepoName: externalRepoName, Uri: uri, } if valid, err := validation.IsValid(result); !valid { @@ -35,6 +39,8 @@ func (user FollowingRepo) Validate() []string { result = append(result, validation.ValidateNotEmpty(user.RepoID, "UserID")...) result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...) result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...) + result = append(result, validation.ValidateNotEmpty(user.ExternalOwner, "ExternalOwner")...) + result = append(result, validation.ValidateNotEmpty(user.ExternalRepoName, "ExternalRepoName")...) result = append(result, validation.ValidateNotEmpty(user.Uri, "Uri")...) return result } diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go index f0d6996bef..342a0bfada 100644 --- a/services/federation/federation_service.go +++ b/services/federation/federation_service.go @@ -5,6 +5,7 @@ package federation import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -40,7 +41,7 @@ func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int // parse actorID (person) actorURI := activity.Actor.GetID().String() log.Info("actorURI was: %v", actorURI) - federationHost, err := GetFederationHostForUri(ctx, actorURI) + federationHost, err := GetFederationHostFromUri(ctx, actorURI) if err != nil { return http.StatusInternalServerError, "Wrong FederationHost", err } @@ -129,7 +130,7 @@ func CreateFederationHostFromAP(ctx context.Context, actorID forgefed.ActorID) ( return &result, nil } -func GetFederationHostForUri(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) { +func GetFederationHostFromUri(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) { // parse actorID (person) log.Info("Input was: %v", actorURI) rawActorID, err := forgefed.NewActorID(actorURI) @@ -213,19 +214,91 @@ func CreateUserFromAP(ctx context.Context, personID forgefed.PersonID, federatio return &newUser, &federatedUser, nil } +// TODO: This needs probably an own struct/value object +// ==================================================== +func GetRepoOwnerAndNameFromRepoUri(uri string) (string, string, error) { + path, err := getUriPathFromRepoUri(uri) + if err != nil { + return "", "", err + } + pathSplit := strings.Split(path, "/") + + return pathSplit[0], pathSplit[1], nil +} + +func GetRepoAPIUriFromRepoUri(uri string) (string, error) { + path, err := getUriPathFromRepoUri(uri) + if err != nil { + return "", err + } + + parsedUri, err := url.ParseRequestURI(uri) + if err != nil { + return "", err + } + parsedUri.Path = fmt.Sprintf("%v/%v", "api/v1/repos", path) + + return parsedUri.String(), nil +} + +func getUriPathFromRepoUri(uri string) (string, error) { + if !IsValidRepoUri(uri) { + return "", errors.New("malformed repository uri") + } + parsedUri, err := url.ParseRequestURI(uri) + if err != nil { + return "", err + } + path := strings.TrimPrefix(parsedUri.Path, "/") + return path, nil +} + +// TODO: This belongs into some value object +func IsValidRepoUri(uri string) bool { + parsedUri, err := url.ParseRequestURI(uri) + if err != nil { + return false + } + path := strings.TrimPrefix(parsedUri.Path, "/") + pathSplit := strings.Split(path, "/") + if len(pathSplit) != 2 { + return false + } + for _, part := range pathSplit { + if strings.TrimSpace(part) == "" { + return false + } + } + return true +} + +// ==================================================== + // Create or update a list of FollowingRepo structs func StoreFollowingRepoList(ctx context.Context, localRepoId int64, followingRepoList []string) (int, string, error) { followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList)) for _, uri := range followingRepoList { - federationHost, err := GetFederationHostForUri(ctx, uri) + owner, repoName, err := GetRepoOwnerAndNameFromRepoUri(uri) + if err != nil { + return http.StatusBadRequest, "Malformed Repository URI", err + } + repoApiUri, err := GetRepoAPIUriFromRepoUri(uri) + if err != nil { + return http.StatusBadRequest, "Malformed Repository URI", err + } + log.Info("RepoApiUri: %q", repoApiUri) + + // TODO: derive host from repoApiUri or derive APAPI URI from repoApiUri + federationHost, err := GetFederationHostFromUri(ctx, repoApiUri) if err != nil { return http.StatusInternalServerError, "Wrong FederationHost", err } - followingRepoID, err := forgefed.NewRepositoryID(uri, string(federationHost.NodeInfo.Source)) + // TODO: derive id from repoApiUri + followingRepoID, err := forgefed.NewRepositoryID(repoApiUri, string(federationHost.NodeInfo.Source)) if err != nil { return http.StatusNotAcceptable, "Invalid federated repo", err } - followingRepo, err := repo.NewFollowingRepo(localRepoId, followingRepoID.ID, federationHost.ID, uri) + followingRepo, err := repo.NewFollowingRepo(localRepoId, followingRepoID.ID, federationHost.ID, owner, repoName, uri) if err != nil { return http.StatusNotAcceptable, "Invalid federated repo", err } diff --git a/services/federation/federation_service_test.go b/services/federation/federation_service_test.go new file mode 100644 index 0000000000..fc409a8ac6 --- /dev/null +++ b/services/federation/federation_service_test.go @@ -0,0 +1,39 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package federation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_IsValidRepoUri(t *testing.T) { + validUri := "http://localhost:3000/me/test" + invalidUri := "http://localhost:3000/me/test/foo" + assert.True(t, IsValidRepoUri(validUri)) + assert.False(t, IsValidRepoUri(invalidUri)) +} + +func Test_GetRepoAPIUriFromRepoUri(t *testing.T) { + uri := "http://localhost:3000/me/test" + expectedUri := "http://localhost:3000/api/v1/repos/me/test" + + res, err := GetRepoAPIUriFromRepoUri(uri) + assert.ErrorIs(t, err, nil) + assert.Equal(t, expectedUri, res) +} + +func Test_GetRepoOwnerAndNameFromRepoUri(t *testing.T) { + validUri := "http://localhost:3000/me/test" + invalidUri := "http://localhost:3000/me/test/foo" + + owner, name, err := GetRepoOwnerAndNameFromRepoUri(validUri) + assert.ErrorIs(t, err, nil) + assert.Equal(t, "me", owner) + assert.Equal(t, "test", name) + + _, _, err = GetRepoOwnerAndNameFromRepoUri(invalidUri) + assert.Error(t, err) +}