diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index ebe0266d1f..97f9bb7f06 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -81,6 +81,10 @@ var microcmdUserCreate = &cli.Command{ Name: "restricted", Usage: "Make a restricted user account", }, + &cli.StringFlag{ + Name: "fullname", + Usage: `The full, human-readable name of the user`, + }, }, } @@ -191,6 +195,7 @@ func runCreateUser(c *cli.Context) error { Passwd: password, MustChangePassword: mustChangePassword, Visibility: visibility, + FullName: c.String("fullname"), } overwriteDefault := &user_model.CreateUserOverwriteOptions{ diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go index 6fd6f8c226..d5952412c3 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -50,17 +50,17 @@ func TestAdminUserCreate(t *testing.T) { assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false")) }) - createUser := func(name, args string) error { - return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args))) + createUser := func(name string, args ...string) error { + return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...)) } t.Run("UserType", func(t *testing.T) { reset() - assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type") - assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users") - assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users") + assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type") + assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users") + assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users") - assert.NoError(t, createUser("u", "--user-type bot")) + assert.NoError(t, createUser("u", "--user-type", "bot")) u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}) assert.Equal(t, user_model.UserTypeBot, u.Type) assert.Empty(t, u.Passwd) @@ -75,7 +75,7 @@ func TestAdminUserCreate(t *testing.T) { // using "--access-token" only means "all" access reset() - assert.NoError(t, createUser("u", "--random-password --access-token")) + assert.NoError(t, createUser("u", "--random-password", "--access-token")) assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"}) @@ -85,7 +85,7 @@ func TestAdminUserCreate(t *testing.T) { // using "--access-token" with name & scopes reset() - assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user")) + assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user")) assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"}) @@ -98,23 +98,38 @@ func TestAdminUserCreate(t *testing.T) { // using "--access-token-name" without "--access-token" reset() - err = createUser("u", "--random-password --access-token-name new-token-name") + err = createUser("u", "--random-password", "--access-token-name", "new-token-name") assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set") // using "--access-token-scopes" without "--access-token" reset() - err = createUser("u", "--random-password --access-token-scopes read:issue") + err = createUser("u", "--random-password", "--access-token-scopes", "read:issue") assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set") // empty permission reset() - err = createUser("u", "--random-password --access-token --access-token-scopes public-only") + err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only") assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) assert.ErrorContains(t, err, "access token does not have any permission") }) + + t.Run("UserFields", func(t *testing.T) { + reset() + assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last")) + unittest.AssertExistsAndLoadBean(t, &user_model.User{ + Name: "u-FullNameWithSpace", + LowerName: "u-fullnamewithspace", + FullName: "First O'Middle Last", + Email: "u-FullNameWithSpace@gitea.local", + }) + + assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", "")) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"}) + assert.Empty(t, u.FullName) + }) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c10de95953..53e25a8c3b 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -59,27 +59,16 @@ RUN_USER = ; git ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http' -;; Note: Value must be lowercase. +;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix". ;PROTOCOL = http ;; -;; Expect PROXY protocol headers on connections -;USE_PROXY_PROTOCOL = false -;; -;; Use PROXY protocol in TLS Bridging mode -;PROXY_PROTOCOL_TLS_BRIDGING = false -;; -; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) -;PROXY_PROTOCOL_HEADER_TIMEOUT=5s -;; -; Accept PROXY protocol headers with UNKNOWN type -;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false -;; -;; Set the domain for the server +;; Set the domain for the server. +;; Most users should set it to the real website domain of their Gitea instance. ;DOMAIN = localhost ;; ;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". -;; Most users should set it to the real website URL of their Gitea instance. +;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. +;; When it is empty, Gitea will use HTTP "Host" header to generate ROOT_URL, and fall back to the default one if no "Host" header. ;ROOT_URL = ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. @@ -90,13 +79,25 @@ RUN_USER = ; git ;STATIC_URL_PREFIX = ;; ;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket. -;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use. +;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use. ;; Relative paths will be made absolute against the _`AppWorkPath`_. ;HTTP_ADDR = 0.0.0.0 ;; -;; The port to listen on. Leave empty when using a unix socket. +;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket. ;HTTP_PORT = 3000 ;; +;; Expect PROXY protocol headers on connections +;USE_PROXY_PROTOCOL = false +;; +;; Use PROXY protocol in TLS Bridging mode +;PROXY_PROTOCOL_TLS_BRIDGING = false +;; +;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) +;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s +;; +;; Accept PROXY protocol headers with UNKNOWN type +;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false +;; ;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server ;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main ;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for diff --git a/go.mod b/go.mod index bbe0d5a4c9..496f387f98 100644 --- a/go.mod +++ b/go.mod @@ -120,7 +120,7 @@ require ( gitlab.com/gitlab-org/api/client-go v0.126.0 golang.org/x/crypto v0.36.0 golang.org/x/image v0.25.0 - golang.org/x/net v0.37.0 + golang.org/x/net v0.38.0 golang.org/x/oauth2 v0.28.0 golang.org/x/sync v0.12.0 golang.org/x/sys v0.31.0 diff --git a/go.sum b/go.sum index 9dbb7df060..4d5f37d697 100644 --- a/go.sum +++ b/go.sum @@ -875,8 +875,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/models/actions/runner.go b/models/actions/runner.go index 0411a48393..b55723efa0 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -123,8 +124,15 @@ func (r *ActionRunner) IsOnline() bool { return false } -// Editable checks if the runner is editable by the user -func (r *ActionRunner) Editable(ownerID, repoID int64) bool { +// EditableInContext checks if the runner is editable by the "context" owner/repo +// ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited +// ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited +// ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited +// ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it +func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } if ownerID == 0 && repoID == 0 { return true } @@ -168,6 +176,12 @@ func init() { db.RegisterModel(&ActionRunner{}) } +// FindRunnerOptions +// ownerID == 0 and repoID == 0 means any runner including global runners +// repoID != 0 and WithAvailable == false means any runner for the given repo +// repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners +// ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org +// ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners type FindRunnerOptions struct { db.ListOptions IDs []int64 diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml new file mode 100644 index 0000000000..dce2d41cfb --- /dev/null +++ b/models/fixtures/action_runner.yml @@ -0,0 +1,40 @@ +- + id: 34346 + name: runner_to_be_deleted-user + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 + version: "1.0.0" + owner_id: 1 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34347 + name: runner_to_be_deleted-org + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 + version: "1.0.0" + owner_id: 3 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34348 + name: runner_to_be_deleted-repo1 + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 + version: "1.0.0" + owner_id: 0 + repo_id: 1 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34349 + name: runner_to_be_deleted + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 + version: "1.0.0" + owner_id: 0 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 278e8e3a86..bb7fd895f8 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { default: e.Desc("package_version.created_unix") } - - // Sort by id for stable order with duplicates in the other field - e.Asc("package_version.id") + e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field } // SearchVersions gets all versions of packages matching the search options diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 5d5b64dc0c..dabc1f5f45 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -70,11 +70,16 @@ func GuessCurrentHostURL(ctx context.Context) string { // 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly. // 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx. // 3. There is no reverse proxy. - // Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3, - // then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users. - // So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL. + // Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in + // wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users. + // So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty reqScheme := getRequestScheme(req) if reqScheme == "" { + // if no reverse proxy header, try to use "Host" header for absolute URL + if setting.UseHostHeader && req.Host != "" { + return util.Iif(req.TLS == nil, "http://", "https://") + req.Host + } + // fall back to default AppURL return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } // X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header. diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index d57653646b..0e198d7d73 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -5,6 +5,7 @@ package httplib import ( "context" + "crypto/tls" "net/http" "testing" @@ -39,6 +40,25 @@ func TestIsRelativeURL(t *testing.T) { } } +func TestGuessCurrentHostURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + defer test.MockVariableValue(&setting.UseHostHeader, false)() + + ctx := t.Context() + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + defer test.MockVariableValue(&setting.UseHostHeader, true)() + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"}) + assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}}) + assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx)) +} + func TestMakeAbsoluteURL(t *testing.T) { defer test.MockVariableValue(&setting.Protocol, "http")() defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() diff --git a/modules/markup/html.go b/modules/markup/html.go index 0e074cbcfa..7c3bd93699 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -71,7 +71,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType { // it is still accepted by the CommonMark specification, as well as the HTML5 spec: // http://spec.commonmark.org/0.28/#email-address // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) - v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") + // At the moment, we use stricter rule for rendering purpose: only allow the "name" part starting after the word boundary + v.emailRegex = regexp.MustCompile(`\b([-\w.!#$%&'*+/=?^{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)\b`) // emojiShortCodeRegex find emoji by alias like :smile: v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) diff --git a/modules/markup/html_email.go b/modules/markup/html_email.go index cbfae8b829..cf18e99d98 100644 --- a/modules/markup/html_email.go +++ b/modules/markup/html_email.go @@ -3,7 +3,11 @@ package markup -import "golang.org/x/net/html" +import ( + "strings" + + "golang.org/x/net/html" +) // emailAddressProcessor replaces raw email addresses with a mailto: link. func emailAddressProcessor(ctx *RenderContext, node *html.Node) { @@ -14,6 +18,14 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) { return } + var nextByte byte + if len(node.Data) > m[3] { + nextByte = node.Data[m[3]] + } + if strings.IndexByte(":/", nextByte) != -1 { + // for cases: "git@gitea.com:owner/repo.git", "https://git@gitea.com/owner/repo.git" + return + } mail := node.Data[m[2]:m[3]] replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/)) node = node.NextSibling.NextSibling diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index aab9fddd91..58f71bdd7b 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -225,10 +225,10 @@ func TestRender_email(t *testing.T) { test := func(input, expected string) { res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res), "input: %s", input) } - // Text that should be turned into email link + // Text that should be turned into email link test( "info@gitea.com", `

info@gitea.com

`) @@ -260,28 +260,48 @@ func TestRender_email(t *testing.T) { j.doe@example.com? j.doe@example.com!

`) + // match GitHub behavior + test("email@domain@domain.com", `

email@domain@domain.com

`) + + // match GitHub behavior + test(`"info@gitea.com"`, `

"info@gitea.com"

`) + // Test that should *not* be turned into email links - test( - "\"info@gitea.com\"", - `

"info@gitea.com"

`) test( "/home/gitea/mailstore/info@gitea/com", `

/home/gitea/mailstore/info@gitea/com

`) test( "git@try.gitea.io:go-gitea/gitea.git", `

git@try.gitea.io:go-gitea/gitea.git

`) + test( + "https://foo:bar@gitea.io", + `

https://foo:bar@gitea.io

`) test( "gitea@3", `

gitea@3

`) test( "gitea@gmail.c", `

gitea@gmail.c

`) - test( - "email@domain@domain.com", - `

email@domain@domain.com

`) test( "email@domain..com", `

email@domain..com

`) + + cases := []struct { + input, expected string + }{ + // match GitHub behavior + {"?a@d.zz", `

?a@d.zz

`}, + {"*a@d.zz", `

*a@d.zz

`}, + {"~a@d.zz", `

~a@d.zz

`}, + + // the following cases don't match GitHub behavior, but they are valid email addresses ... + // maybe we should reduce the candidate characters for the "name" part in the future + {"a*a@d.zz", `

a*a@d.zz

`}, + {"a~a@d.zz", `

a~a@d.zz

`}, + } + for _, c := range cases { + test(c.input, c.expected) + } } func TestRender_emoji(t *testing.T) { diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 0d7180c6b1..79df547c2c 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -86,20 +86,15 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C preClasses += " is-loading" } - err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
`, preClasses)
-		if err != nil {
-			return
-		}
-
 		// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
 		// the "display" class is used by "js/markup/math.ts" to render the code element as a block
 		// the "math.ts" strictly depends on the structure: 
...
- err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, languageStr) + err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
`, preClasses, languageStr)
 		if err != nil {
 			return
 		}
 	} else {
-		_, err := w.WriteString("
") + _, err := w.WriteString("
") if err != nil { return } diff --git a/modules/setting/server.go b/modules/setting/server.go index ca635c8abe..41b0ca8959 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -46,25 +46,37 @@ var ( // AppURL is the Application ROOT_URL. It always has a '/' suffix // It maps to ini:"ROOT_URL" AppURL string - // AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. + + // AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL" + // It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'. // This value is empty if site does not have sub-url. AppSubURL string - // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy. + + // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", + // to make it easier to debug sub-path related problems without a reverse proxy. UseSubURLPath bool + + // UseHostHeader makes Gitea prefer to use the "Host" request header for construction of absolute URLs. + UseHostHeader bool + // AppDataPath is the default path for storing data. // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" AppDataPath string + // LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string - // AssetVersion holds a opaque value that is used for cache-busting assets + + // AssetVersion holds an opaque value that is used for cache-busting assets AssetVersion string - appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir + // appTempPathInternal is the temporary path for the app, it is only an internal variable + // DO NOT use it directly, always use AppDataTempDir + appTempPathInternal string Protocol Scheme - UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` - ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` + UseProxyProtocol bool + ProxyProtocolTLSBridging bool ProxyProtocolHeaderTimeout time.Duration ProxyProtocolAcceptUnknown bool Domain string @@ -181,13 +193,14 @@ func loadServerFrom(rootCfg ConfigProvider) { EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) } - Protocol = HTTP protocolCfg := sec.Key("PROTOCOL").String() if protocolCfg != "https" && EnableAcme { log.Fatal("ACME could only be used with HTTPS protocol") } switch protocolCfg { + case "", "http": + Protocol = HTTP case "https": Protocol = HTTPS if EnableAcme { @@ -243,7 +256,7 @@ func loadServerFrom(rootCfg ConfigProvider) { case "unix": log.Warn("unix PROTOCOL value is deprecated, please use http+unix") fallthrough - case "http+unix": + default: // "http+unix" Protocol = HTTPUnix } UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666") @@ -256,6 +269,8 @@ func loadServerFrom(rootCfg ConfigProvider) { if !filepath.IsAbs(HTTPAddr) { HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) } + default: + log.Fatal("Invalid PROTOCOL %q", Protocol) } UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false) ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false) @@ -268,12 +283,16 @@ func loadServerFrom(rootCfg ConfigProvider) { PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort - AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) + AppURL = sec.Key("ROOT_URL").String() + if AppURL == "" { + UseHostHeader = true + AppURL = defaultAppURL + } // Check validity of AppURL appURL, err := url.Parse(AppURL) if err != nil { - log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err) + log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err) } // Remove default ports from AppURL. // (scheme-based URL normalization, RFC 3986 section 6.2.3) @@ -309,13 +328,15 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultLocalURL = AppURL case FCGIUnix: defaultLocalURL = AppURL - default: + case HTTP, HTTPS: defaultLocalURL = string(Protocol) + "://" if HTTPAddr == "0.0.0.0" { defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/" } else { defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/" } + default: + log.Fatal("Invalid PROTOCOL %q", Protocol) } LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) LocalURL = strings.TrimRight(LocalURL, "/") + "/" diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 22409b4aff..75f8e188dd 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -133,3 +133,26 @@ type ActionWorkflowJob struct { // swagger:strfmt date-time CompletedAt time.Time `json:"completed_at,omitempty"` } + +// ActionRunnerLabel represents a Runner Label +type ActionRunnerLabel struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ActionRunner represents a Runner +type ActionRunner struct { + ID int64 `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Busy bool `json:"busy"` + Ephemeral bool `json:"ephemeral"` + Labels []*ActionRunnerLabel `json:"labels"` +} + +// ActionRunnersResponse returns Runners +type ActionRunnersResponse struct { + Entries []*ActionRunner `json:"runners"` + TotalCount int64 `json:"total_count"` +} diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index e98c453744..58069f8613 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -117,6 +117,7 @@ files=ファイル error=エラー error404=アクセスしようとしたページは存在しないか、閲覧が許可されていません。 +error503=サーバーはリクエストを完了できませんでした。 後でもう一度お試しください。 go_back=戻る invalid_data=無効なデータ: %v @@ -730,6 +731,8 @@ public_profile=公開プロフィール biography_placeholder=自己紹介してください!(Markdownを使うことができます) location_placeholder=おおよその場所を他の人と共有 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 +password_username_disabled=ユーザー名の変更は許可されていません。詳細はサイト管理者にお問い合わせください。 +password_full_name_disabled=フルネームの変更は許可されていません。詳細はサイト管理者にお問い合わせください。 full_name=フルネーム website=Webサイト location=場所 @@ -924,6 +927,9 @@ permission_not_set=設定なし permission_no_access=アクセス不可 permission_read=読み取り permission_write=読み取りと書き込み +permission_anonymous_read=匿名の読み込み +permission_everyone_read=全員の読み込み +permission_everyone_write=全員の書き込み access_token_desc=選択したトークン権限に応じて、関連するAPIルートのみに許可が制限されます。 詳細はドキュメントを参照してください。 at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります permissions_list=許可: @@ -1136,6 +1142,7 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま desc.private=プライベート desc.public=公開 +desc.public_access=公開アクセス desc.template=テンプレート desc.internal=内部 desc.archived=アーカイブ @@ -1544,6 +1551,7 @@ issues.filter_project=プロジェクト issues.filter_project_all=すべてのプロジェクト issues.filter_project_none=プロジェクトなし issues.filter_assignee=担当者 +issues.filter_assignee_no_assignee=担当者なし issues.filter_assignee_any_assignee=担当者あり issues.filter_poster=作成者 issues.filter_user_placeholder=ユーザーを検索 @@ -1647,6 +1655,8 @@ issues.label_archived_filter=アーカイブされたラベルを表示 issues.label_archive_tooltip=アーカイブされたラベルは、ラベルによる検索時のサジェストからデフォルトで除外されます。 issues.label_exclusive_desc=ラベル名を スコープ/アイテム の形にすることで、他の スコープ/ ラベルと排他的になります。 issues.label_exclusive_warning=イシューやプルリクエストのラベル編集では、競合するスコープ付きラベルは解除されます。 +issues.label_exclusive_order=ソート順 +issues.label_exclusive_order_tooltip=同じスコープ内の排他的なラベルは、この数値順にソートされます。 issues.label_count=ラベル %d件 issues.label_open_issues=オープン中のイシュー %d件 issues.label_edit=編集 @@ -2129,6 +2139,12 @@ contributors.contribution_type.deletions=削除 settings=設定 settings.desc=設定では、リポジトリの設定を管理することができます。 settings.options=リポジトリ +settings.public_access=公開アクセス +settings.public_access_desc=外部からの訪問者のアクセス権限について、このリポジトリのデフォルト設定を上書きします。 +settings.public_access.docs.not_set=設定なし: 公開アクセス権限はありません。訪問者の権限は、リポジトリの公開範囲とメンバーの権限に従います。 +settings.public_access.docs.anonymous_read=匿名の読み込み: ログインしていないユーザーは読み取り権限でユニットにアクセスできます。 +settings.public_access.docs.everyone_read=全員の読み込み: すべてのログインユーザーは読み取り権限でユニットにアクセスできます。イシュー/プルリクエストユニットの読み取り権限は、ユーザーが新しいイシュー/プルリクエストを作成できることを意味します。 +settings.public_access.docs.everyone_write=全員の書き込み: すべてのログインユーザーに書き込み権限があります。Wikiユニットのみがこの権限をサポートします。 settings.collaboration=共同作業者 settings.collaboration.admin=管理者 settings.collaboration.write=書き込み @@ -2719,6 +2735,7 @@ branch.restore_success=ブランチ "%s" を復元しました。 branch.restore_failed=ブランチ "%s" の復元に失敗しました。 branch.protected_deletion_failed=ブランチ "%s" は保護されています。 削除できません。 branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチです。 削除できません。 +branch.default_branch_not_exist=デフォルトブランチ "%s" がありません。 branch.restore=ブランチ "%s" の復元 branch.download=ブランチ "%s" をダウンロード branch.rename=ブランチ名 "%s" を変更 diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 4d7fb8b1a6..47439c4c3b 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) { }) } -// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 +// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present. +func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) { + multipartFile, _, err := ctx.Req.FormFile(formKey) + if err != nil && !errors.Is(err, http.ErrMissingFile) { + return nil, err + } + if multipartFile != nil { + return multipartFile, nil + } + + content := ctx.Req.FormValue(formKey) + if content == "" { + return nil, nil + } + return io.NopCloser(strings.NewReader(content)), nil +} + +// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 func UploadPackageFile(ctx *context.Context) { packageScope := ctx.PathParam("scope") packageName := ctx.PathParam("name") @@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) { packageVersion := v.Core().String() - file, _, err := ctx.Req.FormFile("source-archive") - if err != nil { - apiError(ctx, http.StatusBadRequest, err) + file, err := formFileOptionalReadCloser(ctx, "source-archive") + if file == nil || err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read source-archive file") return } defer file.Close() @@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) { } defer buf.Close() - var mr io.Reader - metadata := ctx.Req.FormValue("metadata") - if metadata != "" { - mr = strings.NewReader(metadata) + mr, err := formFileOptionalReadCloser(ctx, "metadata") + if err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read metadata file") + return + } + if mr != nil { + defer mr.Close() } pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 329242d9f6..736c421229 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, 0) } + +// CreateRegistrationToken returns the token to register global runners +func CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken + // --- + // summary: Get an global actions runner registration token + // produces: + // - application/json + // parameters: + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, 0, 0) +} + +// ListRunners get all runners +func ListRunners(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runners admin getAdminRunners + // --- + // summary: Get all runners + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, 0, 0) +} + +// GetRunner get an global runner +func GetRunner(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner + // --- + // summary: Get an global runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an global runner +func DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner + // --- + // summary: Delete an global runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b9b590725b..e77118f4ff 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -912,7 +912,11 @@ func Routes() *web.Router { }) m.Group("/runners", func() { + m.Get("", reqToken(), reqChecker, act.ListRunners) m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken) + m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) + m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) }) }) } @@ -1043,7 +1047,11 @@ func Routes() *web.Router { }) m.Group("/runners", func() { + m.Get("", reqToken(), user.ListRunners) m.Get("/registration-token", reqToken(), user.GetRegistrationToken) + m.Post("/registration-token", reqToken(), user.CreateRegistrationToken) + m.Get("/{runner_id}", reqToken(), user.GetRunner) + m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) }) }) @@ -1689,6 +1697,12 @@ func Routes() *web.Router { Patch(bind(api.EditHookOption{}), admin.EditHook). Delete(admin.DeleteHook) }) + m.Group("/actions/runners", func() { + m.Get("", admin.ListRunners) + m.Post("/registration-token", admin.CreateRegistrationToken) + m.Get("/{runner_id}", admin.GetRunner) + m.Delete("/{runner_id}", admin.DeleteRunner) + }) m.Group("/runners", func() { m.Get("/registration-token", admin.GetRegistrationToken) }) diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index b1cd2f0c3c..700a5ef8ea 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -190,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) } +// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization +// CreateRegistrationToken returns the token to register org runners +func (Action) CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken + // --- + // summary: Get an organization's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) +} + // ListVariables list org-level variables func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList @@ -470,6 +491,85 @@ func (Action) UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// ListRunners get org-level runners +func (Action) ListRunners(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners + // --- + // summary: Get org-level runners + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, ctx.Org.Organization.ID, 0) +} + +// GetRunner get an org-level runner +func (Action) GetRunner(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner + // --- + // summary: Get an org-level runner + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an org-level runner +func (Action) DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner + // --- + // summary: Delete an org-level runner + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index ed2017a372..6aef529f98 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) { // required: true // responses: // "204": - // description: delete one secret of the organization + // description: delete one secret of the repository // "400": // "$ref": "#/responses/error" // "404": @@ -531,6 +531,125 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) } +// CreateRegistrationToken returns the token to register repo runners +func (Action) CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken + // --- + // summary: Get a repository's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) +} + +// ListRunners get repo-level runners +func (Action) ListRunners(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners + // --- + // summary: Get repo-level runners + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID) +} + +// GetRunner get an repo-level runner +func (Action) GetRunner(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner + // --- + // summary: Get an repo-level runner + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an repo-level runner +func (Action) DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner + // --- + // summary: Delete an repo-level runner + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 67dd6c913d..d5840b4149 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) // Get last change information. lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) @@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) page := ctx.FormInt("page") if page <= 1 { @@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) { // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: "master", + Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) @@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) return nil, nil } - commit, err := wikiRepo.GetBranchCommit("master") + commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if err != nil { if git.IsErrNotExist(err) { ctx.APIErrorNotFound(err) diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index f31d9e5d0b..d42f330d1c 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -8,8 +8,13 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" ) // RegistrationToken is response related to registration token @@ -30,3 +35,84 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) { ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token}) } + +// ListRunners lists runners for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause +// ownerID == 0 and repoID != 0 means all runners for the given repo +// ownerID != 0 and repoID == 0 means all runners for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func ListRunners(ctx *context.APIContext, ownerID, repoID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{ + OwnerID: ownerID, + RepoID: repoID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionRunnersResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionRunner, len(runners)) + for i, runner := range runners { + res.Entries[i] = convert.ToActionRunner(ctx, runner) + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetRunner get the runner for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means any runner including global runners +// ownerID == 0 and repoID != 0 means any runner for the given repo +// ownerID != 0 and repoID == 0 means any runner for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + runner, err := actions_model.GetRunnerByID(ctx, runnerID) + if err != nil { + ctx.APIErrorNotFound(err) + return + } + if !runner.EditableInContext(ownerID, repoID) { + ctx.APIErrorNotFound("No permission to get this runner") + return + } + ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner)) +} + +// DeleteRunner deletes the runner for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means any runner including global runners +// ownerID == 0 and repoID != 0 means any runner for the given repo +// ownerID != 0 and repoID == 0 means any runner for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + runner, err := actions_model.GetRunnerByID(ctx, runnerID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if !runner.EditableInContext(ownerID, repoID) { + ctx.APIErrorNotFound("No permission to delete this runner") + return + } + + err = actions_model.DeleteRunner(ctx, runner.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 25f137f3bf..df0c8a805a 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -457,6 +457,20 @@ type swaggerRepoArtifact struct { Body api.ActionArtifact `json:"body"` } +// RunnerList +// swagger:response RunnerList +type swaggerRunnerList struct { + // in:body + Body api.ActionRunnersResponse `json:"body"` +} + +// Runner +// swagger:response Runner +type swaggerRunner struct { + // in:body + Body api.ActionRunner `json:"body"` +} + // swagger:response Compare type swaggerCompare struct { // in:body diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index 899218473e..be3f63cc5e 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) } + +// CreateRegistrationToken returns the token to register user runners +func CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken + // --- + // summary: Get an user's actions runner registration token + // produces: + // - application/json + // parameters: + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) +} + +// ListRunners get user-level runners +func ListRunners(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners user getUserRunners + // --- + // summary: Get user-level runners + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, ctx.Doer.ID, 0) +} + +// GetRunner get an user-level runner +func GetRunner(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner + // --- + // summary: Get an user-level runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an user-level runner +func DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner + // --- + // summary: Delete an user-level runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) +} diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index a568c7c5c8..04fad4663c 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -76,6 +76,7 @@ func TestShadowPassword(t *testing.T) { func TestSelfCheckPost(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + defer test.MockVariableValue(&setting.UseHostHeader, false)() ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") SelfCheckPost(ctx) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0124c163f3..a33542fd37 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -181,6 +181,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { // GetPullDiffStats get Pull Requests diff stats func GetPullDiffStats(ctx *context.Context) { + // FIXME: this getPullInfo seems to be a duplicate call with other route handlers issue, ok := getPullInfo(ctx) if !ok { return @@ -188,21 +189,19 @@ func GetPullDiffStats(ctx *context.Context) { pull := issue.PullRequest mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue) - if mergeBaseCommitID == "" { - ctx.NotFound(nil) - return + return // no merge base, do nothing, do not stop the route handler, see below } + // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view. headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName()) if err != nil { - ctx.ServerError("GetRefCommitID", err) + log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName()) return } - diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID) if err != nil { - ctx.ServerError("GetDiffShortStat", err) + log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName()) return } diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index a87f6ce4dc..a642cfd66d 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -197,7 +197,7 @@ func RunnersEdit(ctx *context.Context) { ctx.ServerError("LoadAttributes", err) return } - if !runner.Editable(ownerID, repoID) { + if !runner.EditableInContext(ownerID, repoID) { err = errors.New("no permission to edit this runner") ctx.NotFound(err) return @@ -250,7 +250,7 @@ func RunnersEditPost(ctx *context.Context) { ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err) return } - if !runner.Editable(ownerID, repoID) { + if !runner.EditableInContext(ownerID, repoID) { ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner")) return } @@ -304,7 +304,7 @@ func RunnerDeletePost(ctx *context.Context) { return } - if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) { + if !runner.EditableInContext(rCtx.OwnerID, rCtx.RepoID) { ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to delete this runner")) return } diff --git a/services/actions/interface.go b/services/actions/interface.go index d4fa782fec..b407f5c6c8 100644 --- a/services/actions/interface.go +++ b/services/actions/interface.go @@ -25,4 +25,12 @@ type API interface { UpdateVariable(*context.APIContext) // GetRegistrationToken get registration token GetRegistrationToken(*context.APIContext) + // CreateRegistrationToken get registration token + CreateRegistrationToken(*context.APIContext) + // ListRunners list runners + ListRunners(*context.APIContext) + // GetRunner get a runner + GetRunner(*context.APIContext) + // DeleteRunner delete runner + DeleteRunner(*context.APIContext) } diff --git a/services/convert/convert.go b/services/convert/convert.go index ac2680766c..9d2afdea30 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -30,6 +30,8 @@ import ( "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/gitdiff" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" ) // ToEmail convert models.EmailAddress to api.Email @@ -252,6 +254,30 @@ func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArti }, nil } +func ToActionRunner(ctx context.Context, runner *actions_model.ActionRunner) *api.ActionRunner { + status := runner.Status() + apiStatus := "offline" + if runner.IsOnline() { + apiStatus = "online" + } + labels := make([]*api.ActionRunnerLabel, len(runner.AgentLabels)) + for i, label := range runner.AgentLabels { + labels[i] = &api.ActionRunnerLabel{ + ID: int64(i), + Name: label, + Type: "custom", + } + } + return &api.ActionRunner{ + ID: runner.ID, + Name: runner.Name, + Status: apiStatus, + Busy: status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE, + Ephemeral: runner.Ephemeral, + Labels: labels, + } +} + // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { verif := asymkey_service.ParseCommitWithSignature(ctx, c) diff --git a/services/feed/feed.go b/services/feed/feed.go index 38a4e25308..1dbd2e0e26 100644 --- a/services/feed/feed.go +++ b/services/feed/feed.go @@ -6,7 +6,7 @@ package feed import ( "context" "fmt" - "strconv" + "strings" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" @@ -14,15 +14,10 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) -func userFeedCacheKey(userID int64) string { - return fmt.Sprintf("user_feed_%d", userID) -} - func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) { opts.DontCount = opts.RequestedTeam == nil && opts.Date == "" results, cnt, err := activities_model.GetFeeds(ctx, opts) @@ -40,7 +35,18 @@ func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activ // * Organization action: UserID=100 (the repo's org), ActUserID=1 // * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers []*repo_model.Watch, permCode, permIssue, permPR []bool) error { - // Add feed for actioner. + // MySQL has TEXT length limit 65535. + // Sometimes the content is "field1|field2|field3", sometimes the content is JSON (ActionMirrorSyncPush, ActionCommitRepo, ActionPushTag, etc...) + if left, right := util.EllipsisDisplayStringX(act.Content, 65535); right != "" { + if strings.HasPrefix(act.Content, `{"`) && strings.HasSuffix(act.Content, `}`) { + // FIXME: at the moment we can do nothing if the content is JSON and it is too long + act.Content = "{}" + } else { + act.Content = left + } + } + + // Add feed for actor. act.UserID = act.ActUserID if err := db.Insert(ctx, act); err != nil { return fmt.Errorf("insert new actioner: %w", err) @@ -76,24 +82,18 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers if !permPR[i] { continue } + default: } if err := db.Insert(ctx, act); err != nil { return fmt.Errorf("insert new action: %w", err) } - - total, err := activities_model.CountUserFeeds(ctx, act.UserID) - if err != nil { - return fmt.Errorf("count user feeds: %w", err) - } - - _ = cache.GetCache().Put(userFeedCacheKey(act.UserID), strconv.FormatInt(total, 10), setting.CacheService.TTLSeconds()) } return nil } -// NotifyWatchersActions creates batch of actions for every watcher. +// NotifyWatchers creates batch of actions for every watcher. func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error { return db.WithTx(ctx, func(ctx context.Context) error { if len(acts) == 0 { diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index 5b40268761..7a435cc433 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -9,16 +9,16 @@ silenced

Button

-
- Style: - - - - + ".ui.button" styles: +
+ + + +
-
- State: - +
+ +
    diff --git a/templates/devtest/markup-render.tmpl b/templates/devtest/markup-render.tmpl new file mode 100644 index 0000000000..69d29d7829 --- /dev/null +++ b/templates/devtest/markup-render.tmpl @@ -0,0 +1,71 @@ +{{template "devtest/devtest-header"}} +
    + {{$longCode := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}} +
    +
    +
    + Inline code content +
    + +
    + +
    +

    content before

    +
    Very long line with no code block or container: {{$longCode}}
    +

    content after

    +
    + +
    + +
    +

    content before

    +
    +
    Very long line with wrap: {{$longCode}}
    +
    +

    content after

    +
    + +
    + +
    +

    content before

    +
    +
    Short line in scroll container
    +
    +
    +
    Very long line with scroll: {{$longCode}}
    +
    +

    content after

    +
    +
    + +
    +
    +

    content before

    +
    +
    
    +	\lim\limits_{n\rightarrow\infty}{\left(1+\frac{1}{n}\right)^n}
    +					
    +
    +

    content after

    +
    + +
    + +
    +

    content before

    +
    +
    
    +	graph LR
    +			A[Square Rect] -- Link text --> B((Circle))
    +			A --> C(Round Rect)
    +			B --> D{Rhombus}
    +			C --> D
    +					
    +
    +

    content after

    +
    +
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 4e3e388cb6..432df10749 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -4,7 +4,7 @@
    {{template "base/alert" .}} {{if .IsOrganizationOwner}} -
    +
    diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 48083811e7..f6d549a634 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -1,5 +1,5 @@ {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} -
    + {{end}} diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index a936079c46..f2630be09b 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -64,7 +64,7 @@
    -
    +
    {{ctx.Locale.Tr "repo.milestones.cancel"}} diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl index 0e3c13eaa2..e23bc8a19a 100644 --- a/templates/repo/clone_panel.tmpl +++ b/templates/repo/clone_panel.tmpl @@ -1,4 +1,4 @@ -