From 5cd1d6c93ba9b8399f826e671b8940eb5294b872 Mon Sep 17 00:00:00 2001
From: Mai-Lapyst <67418776+Mai-Lapyst@users.noreply.github.com>
Date: Tue, 28 Mar 2023 19:55:03 +0200
Subject: [PATCH] Set repository link based on the url in package.json for npm
 packages (#20379)

automatically set repository link for package based on the repository
url present inside package.json

closes #20146
---
 models/repo/repo.go             | 43 +++++++++++++++++++++++
 models/repo/repo_test.go        | 62 +++++++++++++++++++++++++++++++++
 routers/api/packages/npm/npm.go | 30 ++++++++++++++++
 3 files changed, 135 insertions(+)

diff --git a/models/repo/repo.go b/models/repo/repo.go
index dcffb63fd1..3653dae015 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -658,6 +658,49 @@ func GetRepositoryByName(ownerID int64, name string) (*Repository, error) {
 	return repo, err
 }
 
+// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
+func getRepositoryURLPathSegments(repoURL string) []string {
+	if strings.HasPrefix(repoURL, setting.AppURL) {
+		return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
+	}
+
+	sshURLVariants := [4]string{
+		setting.SSH.Domain + ":",
+		setting.SSH.User + "@" + setting.SSH.Domain + ":",
+		"git+ssh://" + setting.SSH.Domain + "/",
+		"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
+	}
+
+	for _, sshURL := range sshURLVariants {
+		if strings.HasPrefix(repoURL, sshURL) {
+			return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
+		}
+	}
+
+	return nil
+}
+
+// GetRepositoryByURL returns the repository by given url
+func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
+	// possible urls for git:
+	//  https://my.domain/sub-path/<owner>/<repo>.git
+	//  https://my.domain/sub-path/<owner>/<repo>
+	//  git+ssh://user@my.domain/<owner>/<repo>.git
+	//  git+ssh://user@my.domain/<owner>/<repo>
+	//  user@my.domain:<owner>/<repo>.git
+	//  user@my.domain:<owner>/<repo>
+
+	pathSegments := getRepositoryURLPathSegments(repoURL)
+
+	if len(pathSegments) != 2 {
+		return nil, fmt.Errorf("unknown or malformed repository URL")
+	}
+
+	ownerName := pathSegments[0]
+	repoName := strings.TrimSuffix(pathSegments[1], ".git")
+	return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
+}
+
 // GetRepositoryByID returns the repository by given id if exists.
 func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
 	repo := new(Repository)
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index fb473151eb..92a58ea3f9 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -124,3 +124,65 @@ func TestMetas(t *testing.T) {
 	assert.Equal(t, "user3", metas["org"])
 	assert.Equal(t, ",owners,team1,", metas["teams"])
 }
+
+func TestGetRepositoryByURL(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	t.Run("InvalidPath", func(t *testing.T) {
+		repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, "something")
+
+		assert.Nil(t, repo)
+		assert.Error(t, err)
+	})
+
+	t.Run("ValidHttpURL", func(t *testing.T) {
+		test := func(t *testing.T, url string) {
+			repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+
+			assert.NotNil(t, repo)
+			assert.NoError(t, err)
+
+			assert.Equal(t, repo.ID, int64(2))
+			assert.Equal(t, repo.OwnerID, int64(2))
+		}
+
+		test(t, "https://try.gitea.io/user2/repo2")
+		test(t, "https://try.gitea.io/user2/repo2.git")
+	})
+
+	t.Run("ValidGitSshURL", func(t *testing.T) {
+		test := func(t *testing.T, url string) {
+			repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+
+			assert.NotNil(t, repo)
+			assert.NoError(t, err)
+
+			assert.Equal(t, repo.ID, int64(2))
+			assert.Equal(t, repo.OwnerID, int64(2))
+		}
+
+		test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
+		test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git")
+
+		test(t, "git+ssh://try.gitea.io/user2/repo2")
+		test(t, "git+ssh://try.gitea.io/user2/repo2.git")
+	})
+
+	t.Run("ValidImplicitSshURL", func(t *testing.T) {
+		test := func(t *testing.T, url string) {
+			repo, err := repo_model.GetRepositoryByURL(db.DefaultContext, url)
+
+			assert.NotNil(t, repo)
+			assert.NoError(t, err)
+
+			assert.Equal(t, repo.ID, int64(2))
+			assert.Equal(t, repo.OwnerID, int64(2))
+		}
+
+		test(t, "sshuser@try.gitea.io:user2/repo2")
+		test(t, "sshuser@try.gitea.io:user2/repo2.git")
+
+		test(t, "try.gitea.io:user2/repo2")
+		test(t, "try.gitea.io:user2/repo2.git")
+	})
+}
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 0d25f173e9..51b34d3e27 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -13,6 +13,9 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	packages_model "code.gitea.io/gitea/models/packages"
+	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/context"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	npm_module "code.gitea.io/gitea/modules/packages/npm"
@@ -166,6 +169,26 @@ func UploadPackage(ctx *context.Context) {
 		return
 	}
 
+	repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL)
+	if err == nil {
+		canWrite := repo.OwnerID == ctx.Doer.ID
+
+		if !canWrite {
+			perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
+			if err != nil {
+				apiError(ctx, http.StatusInternalServerError, err)
+				return
+			}
+
+			canWrite = perms.CanWrite(unit.TypePackages)
+		}
+
+		if !canWrite {
+			apiError(ctx, http.StatusForbidden, "no permission to upload this package")
+			return
+		}
+	}
+
 	buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data), 32*1024*1024)
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
@@ -217,6 +240,13 @@ func UploadPackage(ctx *context.Context) {
 		}
 	}
 
+	if repo != nil {
+		if err := packages_model.SetRepositoryLink(ctx, pv.PackageID, repo.ID); err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+	}
+
 	ctx.Status(http.StatusCreated)
 }