0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-20 10:28:36 +02:00

Merge branch 'main' into lunny/refactor_org_setting

This commit is contained in:
Lunny Xiao 2025-07-06 10:54:07 -07:00
commit 4db1324892
59 changed files with 281 additions and 206 deletions

View File

@ -50,6 +50,8 @@ linters:
require-explanation: true require-explanation: true
require-specific: true require-specific: true
gocritic: gocritic:
enabled-checks:
- equalFold
disabled-checks: disabled-checks:
- ifElseChain - ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way. - singleCaseSwitch # Every time this occurred in the code, there was no other way.

View File

@ -30,7 +30,7 @@ These are the values to which people in the Gitea community should aspire.
- **Be constructive.** - **Be constructive.**
- Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. - Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
- Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. - Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved.
- Avoid snarking (pithy, unproductive, sniping comments) - Avoid snarking (pithy, unproductive, sniping comments).
- Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. - Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
- Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group). - Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
- **Be responsible.** - **Be responsible.**
@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta
### Our Pledge ### Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
### Our Standards ### Our Standards

View File

@ -80,9 +80,9 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) [![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language, ask one of the managers in the Crowdin project to add a new language there.
You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty, but we hope to fill it as questions pop up.
Get more information from [documentation](https://docs.gitea.com/contributing/localization). Get more information from [documentation](https://docs.gitea.com/contributing/localization).

View File

@ -157,18 +157,17 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
for lang, size := range stats { for lang, size := range stats {
if size > s { if size > s {
s = size s = size
topLang = strings.ToLower(lang) topLang = lang
} }
} }
for lang, size := range stats { for lang, size := range stats {
upd := false upd := false
llang := strings.ToLower(lang)
for _, s := range oldstats { for _, s := range oldstats {
// Update already existing language // Update already existing language
if strings.ToLower(s.Language) == llang { if strings.EqualFold(s.Language, lang) {
s.CommitID = commitID s.CommitID = commitID
s.IsPrimary = llang == topLang s.IsPrimary = lang == topLang
s.Size = size s.Size = size
if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil { if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil {
return err return err
@ -182,7 +181,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
if err := db.Insert(ctx, &LanguageStat{ if err := db.Insert(ctx, &LanguageStat{
RepoID: repo.ID, RepoID: repo.ID,
CommitID: commitID, CommitID: commitID,
IsPrimary: llang == topLang, IsPrimary: lang == topLang,
Language: lang, Language: lang,
Size: size, Size: size,
}); err != nil { }); err != nil {

View File

@ -91,8 +91,7 @@ func (r *stripRenderer) processAutoLink(w io.Writer, link []byte) {
} }
// Note: we're not attempting to match the URL scheme (http/https) // Note: we're not attempting to match the URL scheme (http/https)
host := strings.ToLower(u.Host) if u.Host != "" && !strings.EqualFold(u.Host, r.localhost.Host) {
if host != "" && host != strings.ToLower(r.localhost.Host) {
// Process out of band // Process out of band
r.links = append(r.links, linkStr) r.links = append(r.links, linkStr)
return return

View File

@ -88,7 +88,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else if strings.ToLower(hd.Name) == "readme.md" { } else if strings.EqualFold(hd.Name, "readme.md") {
data, err := io.ReadAll(tr) data, err := io.ReadAll(tr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -62,11 +62,11 @@ func (c logCompression) IsValid() bool {
} }
func (c logCompression) IsNone() bool { func (c logCompression) IsNone() bool {
return strings.ToLower(string(c)) == "none" return string(c) == "none"
} }
func (c logCompression) IsZstd() bool { func (c logCompression) IsZstd() bool {
return c == "" || strings.ToLower(string(c)) == "zstd" return c == "" || string(c) == "zstd"
} }
func loadActionsFrom(rootCfg ConfigProvider) error { func loadActionsFrom(rootCfg ConfigProvider) error {

View File

@ -40,7 +40,6 @@ func NewFuncMap() template.FuncMap {
"HTMLFormat": htmlFormat, "HTMLFormat": htmlFormat,
"QueryEscape": queryEscape, "QueryEscape": queryEscape,
"QueryBuild": QueryBuild, "QueryBuild": QueryBuild,
"JSEscape": jsEscapeSafe,
"SanitizeHTML": SanitizeHTML, "SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin, "URLJoin": util.URLJoin,
"DotEscape": dotEscape, "DotEscape": dotEscape,
@ -181,10 +180,6 @@ func htmlFormat(s any, args ...any) template.HTML {
panic(fmt.Sprintf("unexpected type %T", s)) panic(fmt.Sprintf("unexpected type %T", s))
} }
func jsEscapeSafe(s string) template.HTML {
return template.HTML(template.JSEscapeString(s))
}
func queryEscape(s string) template.URL { func queryEscape(s string) template.URL {
return template.URL(url.QueryEscape(s)) return template.URL(url.QueryEscape(s))
} }

View File

@ -57,10 +57,6 @@ func TestSubjectBodySeparator(t *testing.T) {
"Insufficient\n--\nSeparators") "Insufficient\n--\nSeparators")
} }
func TestJSEscapeSafe(t *testing.T) {
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, jsEscapeSafe(`&<>'"`))
}
func TestSanitizeHTML(t *testing.T) { func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)) assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
} }

View File

@ -12,8 +12,7 @@ import (
// SliceContainsString sequential searches if string exists in slice. // SliceContainsString sequential searches if string exists in slice.
func SliceContainsString(slice []string, target string, insensitive ...bool) bool { func SliceContainsString(slice []string, target string, insensitive ...bool) bool {
if len(insensitive) != 0 && insensitive[0] { if len(insensitive) != 0 && insensitive[0] {
target = strings.ToLower(target) return slices.ContainsFunc(slice, func(t string) bool { return strings.EqualFold(t, target) })
return slices.ContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target })
} }
return slices.Contains(slice, target) return slices.Contains(slice, target)

View File

@ -59,7 +59,7 @@ func TimeEstimateParse(timeStr string) (int64, error) {
unit := timeStr[match[4]:match[5]] unit := timeStr[match[4]:match[5]]
found := false found := false
for _, u := range timeStrGlobalVars().units { for _, u := range timeStrGlobalVars().units {
if strings.ToLower(unit) == u.name { if strings.EqualFold(unit, u.name) {
total += amount * u.num total += amount * u.num
found = true found = true
break break

View File

@ -2355,6 +2355,7 @@ settings.payload_url = Target URL
settings.http_method = HTTP Method settings.http_method = HTTP Method
settings.content_type = POST Content Type settings.content_type = POST Content Type
settings.secret = Secret settings.secret = Secret
settings.webhook_secret_desc = If the webhook server supports using secret, you can follow the webhook's manual and fill in a secret here.
settings.slack_username = Username settings.slack_username = Username
settings.slack_icon_url = Icon URL settings.slack_icon_url = Icon URL
settings.slack_color = Color settings.slack_color = Color

View File

@ -145,7 +145,7 @@ func repoAssignment() func(ctx *context.APIContext) {
) )
// Check if the user is the same as the repository owner. // Check if the user is the same as the repository owner.
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
owner = ctx.Doer owner = ctx.Doer
} else { } else {
owner, err = user_model.GetUserByName(ctx, userName) owner, err = user_model.GetUserByName(ctx, userName)

View File

@ -276,7 +276,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden" // "$ref": "#/responses/forbidden"
collaboratorUsername := ctx.PathParam("collaborator") collaboratorUsername := ctx.PathParam("collaborator")
if !ctx.Doer.IsAdmin && ctx.Doer.LowerName != strings.ToLower(collaboratorUsername) && !ctx.IsUserRepoAdmin() { if !ctx.Doer.IsAdmin && !strings.EqualFold(ctx.Doer.LowerName, collaboratorUsername) && !ctx.IsUserRepoAdmin() {
ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
return return
} }

View File

@ -669,7 +669,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
newRepoName = *opts.Name newRepoName = *opts.Name
} }
// Check if repository name has been changed and not just a case change // Check if repository name has been changed and not just a case change
if repo.LowerName != strings.ToLower(newRepoName) { if !strings.EqualFold(repo.LowerName, newRepoName) {
if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
switch { switch {
case repo_model.IsErrRepoAlreadyExist(err): case repo_model.IsErrRepoAlreadyExist(err):

View File

@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -161,9 +162,7 @@ func IntrospectOAuth(ctx *context.Context) {
if err == nil && app != nil { if err == nil && app != nil {
response.Active = true response.Active = true
response.Scope = grant.Scope response.Scope = grant.Scope
response.Issuer = setting.AppURL response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/)
response.Audience = []string{app.ClientID}
response.Subject = strconv.FormatInt(grant.UserID, 10)
} }
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil { if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
response.Username = user.Name response.Username = user.Name
@ -423,7 +422,14 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) { func OIDCWellKnown(ctx *context.Context) {
ctx.Data["SigningKey"] = oauth2_provider.DefaultSigningKey if !setting.OAuth2.Enabled {
http.NotFound(ctx.Resp, ctx.Req)
return
}
jwtRegisteredClaims := oauth2_provider.NewJwtRegisteredClaimsFromUser("well-known", 0, nil)
ctx.Data["OidcIssuer"] = jwtRegisteredClaims.Issuer // use the consistent issuer from the JWT registered claims
ctx.Data["OidcBaseUrl"] = strings.TrimSuffix(setting.AppURL, "/")
ctx.Data["SigningKeyMethodAlg"] = oauth2_provider.DefaultSigningKey.SigningMethod().Alg()
ctx.JSONTemplate("user/auth/oidc_wellknown") ctx.JSONTemplate("user/auth/oidc_wellknown")
} }

View File

@ -249,7 +249,7 @@ func ViewPost(ctx *context_module.Context) {
ID: v.ID, ID: v.ID,
Name: v.Name, Name: v.Name,
Status: v.Status.String(), Status: v.Status.String(),
CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions), CanRerun: resp.State.Run.CanRerun,
Duration: v.Duration().String(), Duration: v.Duration().String(),
}) })
} }
@ -445,7 +445,7 @@ func Rerun(ctx *context_module.Context) {
return return
} }
} }
ctx.JSON(http.StatusOK, struct{}{}) ctx.JSONOK()
return return
} }
@ -460,12 +460,12 @@ func Rerun(ctx *context_module.Context) {
} }
} }
ctx.JSON(http.StatusOK, struct{}{}) ctx.JSONOK()
} }
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error { func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status status := job.Status
if !status.IsDone() { if !status.IsDone() || !job.Run.Status.IsDone() {
return nil return nil
} }

View File

@ -165,7 +165,7 @@ func handleSettingsPostUpdate(ctx *context.Context) {
newRepoName := form.RepoName newRepoName := form.RepoName
// Check if repository name has been changed. // Check if repository name has been changed.
if repo.LowerName != strings.ToLower(newRepoName) { if !strings.EqualFold(repo.LowerName, newRepoName) {
// Close the GitRepo if open // Close the GitRepo if open
if ctx.Repo.GitRepo != nil { if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo.Close()

View File

@ -198,7 +198,6 @@ type webhookParams struct {
URL string URL string
ContentType webhook.HookContentType ContentType webhook.HookContentType
Secret string
HTTPMethod string HTTPMethod string
WebhookForm forms.WebhookForm WebhookForm forms.WebhookForm
Meta any Meta any
@ -237,7 +236,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
URL: params.URL, URL: params.URL,
HTTPMethod: params.HTTPMethod, HTTPMethod: params.HTTPMethod,
ContentType: params.ContentType, ContentType: params.ContentType,
Secret: params.Secret, Secret: params.WebhookForm.Secret,
HookEvent: ParseHookEvent(params.WebhookForm), HookEvent: ParseHookEvent(params.WebhookForm),
IsActive: params.WebhookForm.Active, IsActive: params.WebhookForm.Active,
Type: params.Type, Type: params.Type,
@ -290,7 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) {
w.URL = params.URL w.URL = params.URL
w.ContentType = params.ContentType w.ContentType = params.ContentType
w.Secret = params.Secret w.Secret = params.WebhookForm.Secret
w.HookEvent = ParseHookEvent(params.WebhookForm) w.HookEvent = ParseHookEvent(params.WebhookForm)
w.IsActive = params.WebhookForm.Active w.IsActive = params.WebhookForm.Active
w.HTTPMethod = params.HTTPMethod w.HTTPMethod = params.HTTPMethod
@ -336,7 +335,6 @@ func giteaHookParams(ctx *context.Context) webhookParams {
Type: webhook_module.GITEA, Type: webhook_module.GITEA,
URL: form.PayloadURL, URL: form.PayloadURL,
ContentType: contentType, ContentType: contentType,
Secret: form.Secret,
HTTPMethod: form.HTTPMethod, HTTPMethod: form.HTTPMethod,
WebhookForm: form.WebhookForm, WebhookForm: form.WebhookForm,
} }
@ -364,7 +362,6 @@ func gogsHookParams(ctx *context.Context) webhookParams {
Type: webhook_module.GOGS, Type: webhook_module.GOGS,
URL: form.PayloadURL, URL: form.PayloadURL,
ContentType: contentType, ContentType: contentType,
Secret: form.Secret,
WebhookForm: form.WebhookForm, WebhookForm: form.WebhookForm,
} }
} }

View File

@ -4,10 +4,15 @@
package web package web
import ( import (
"html/template"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
// SwaggerV1Json render swagger v1 json // SwaggerV1Json render swagger v1 json
func SwaggerV1Json(ctx *context.Context) { func SwaggerV1Json(ctx *context.Context) {
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
ctx.JSONTemplate("swagger/v1_json") ctx.JSONTemplate("swagger/v1_json")
} }

View File

@ -241,7 +241,7 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
} }
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
if strings.ToLower(source.UserUID) == "dn" { if strings.EqualFold(source.UserUID, "dn") {
return entry.DN return entry.DN
} }

View File

@ -92,7 +92,7 @@ func (ctx *Context) HTML(status int, name templates.TplName) {
} }
// JSONTemplate renders the template as JSON response // JSONTemplate renders the template as JSON response
// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape // keep in mind that the template is processed in HTML context, so JSON things should be handled carefully, e.g.: use JSEscape
func (ctx *Context) JSONTemplate(tmpl templates.TplName) { func (ctx *Context) JSONTemplate(tmpl templates.TplName) {
t, err := ctx.Render.TemplateLookup(string(tmpl), nil) t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
if err != nil { if err != nil {

View File

@ -208,7 +208,7 @@ func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) {
if len(teamName) > 0 { if len(teamName) > 0 {
teamExists := false teamExists := false
for _, team := range ctx.Org.Teams { for _, team := range ctx.Org.Teams {
if team.LowerName == strings.ToLower(teamName) { if strings.EqualFold(team.LowerName, teamName) {
teamExists = true teamExists = true
ctx.Org.Team = team ctx.Org.Team = team
ctx.Org.IsTeamMember = true ctx.Org.IsTeamMember = true

View File

@ -429,7 +429,7 @@ func RepoAssignment(ctx *Context) {
} }
// Check if the user is the same as the repository owner // Check if the user is the same as the repository owner
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
ctx.Repo.Owner = ctx.Doer ctx.Repo.Owner = ctx.Doer
} else { } else {
ctx.Repo.Owner, err = user_model.GetUserByName(ctx, userName) ctx.Repo.Owner, err = user_model.GetUserByName(ctx, userName)

View File

@ -61,7 +61,7 @@ func UserAssignmentAPI() func(ctx *APIContext) {
func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) { func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) {
username := ctx.PathParam("username") username := ctx.PathParam("username")
if doer != nil && doer.LowerName == strings.ToLower(username) { if doer != nil && strings.EqualFold(doer.LowerName, username) {
contextUser = doer contextUser = doer
} else { } else {
var err error var err error

View File

@ -238,6 +238,7 @@ type WebhookForm struct {
Active bool Active bool
BranchFilter string `binding:"GlobPattern"` BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string AuthorizationHeader string
Secret string
} }
// PushOnly if the hook will be triggered when push // PushOnly if the hook will be triggered when push
@ -260,7 +261,6 @@ type NewWebhookForm struct {
PayloadURL string `binding:"Required;ValidUrl"` PayloadURL string `binding:"Required;ValidUrl"`
HTTPMethod string `binding:"Required;In(POST,GET)"` HTTPMethod string `binding:"Required;In(POST,GET)"`
ContentType int `binding:"Required"` ContentType int `binding:"Required"`
Secret string
WebhookForm WebhookForm
} }
@ -274,7 +274,6 @@ func (f *NewWebhookForm) Validate(req *http.Request, errs binding.Errors) bindin
type NewGogshookForm struct { type NewGogshookForm struct {
PayloadURL string `binding:"Required;ValidUrl"` PayloadURL string `binding:"Required;ValidUrl"`
ContentType int `binding:"Required"` ContentType int `binding:"Required"`
Secret string
WebhookForm WebhookForm
} }

View File

@ -106,6 +106,20 @@ func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
return auth.AccessTokenScopeAll return auth.AccessTokenScopeAll
} }
func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt.NumericDate) jwt.RegisteredClaims {
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
// The issuer value returned MUST be identical to the Issuer URL that was used as the prefix to /.well-known/openid-configuration
// to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer.
// * https://accounts.google.com/.well-known/openid-configuration
// * https://github.com/login/oauth/.well-known/openid-configuration
return jwt.RegisteredClaims{
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
Audience: []string{clientID},
Subject: strconv.FormatInt(grantUserID, 10),
ExpiresAt: exp,
}
}
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) { func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens { if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil { if err := grant.IncreaseCounter(ctx); err != nil {
@ -176,13 +190,8 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
} }
idToken := &OIDCToken{ idToken := &OIDCToken{
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, jwt.NewNumericDate(expirationDate.AsTime())),
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()), Nonce: grant.Nonce,
Issuer: setting.AppURL,
Audience: []string{app.ClientID},
Subject: strconv.FormatInt(grant.UserID, 10),
},
Nonce: grant.Nonce,
} }
if grant.ScopeContains("profile") { if grant.ScopeContains("profile") {
idToken.Name = user.DisplayName() idToken.Name = user.DisplayName()

View File

@ -144,7 +144,7 @@ func CleanGitTreePath(name string) string {
name = util.PathJoinRel(name) name = util.PathJoinRel(name)
// Git disallows any filenames to have a .git directory in them. // Git disallows any filenames to have a .git directory in them.
for part := range strings.SplitSeq(name, "/") { for part := range strings.SplitSeq(name, "/") {
if strings.ToLower(part) == ".git" { if strings.EqualFold(part, ".git") {
return "" return ""
} }
} }

View File

@ -1,9 +1,6 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin hooks")}} {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin hooks")}}
<div class="admin-setting-content"> <div class="admin-setting-content">
{{template "repo/settings/webhook/base_list" .SystemWebhooks}} {{template "repo/settings/webhook/base_list" .SystemWebhooks}}
{{template "repo/settings/webhook/base_list" .DefaultWebhooks}} {{template "repo/settings/webhook/base_list" .DefaultWebhooks}}
{{template "repo/settings/webhook/delete_modal" .}}
</div> </div>
{{template "admin/layout_footer" .}} {{template "admin/layout_footer" .}}

View File

@ -1,5 +1,5 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings webhooks")}} {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings webhooks")}}
<div class="org-setting-content"> <div class="org-setting-content">
{{template "repo/settings/webhook/list" .}} {{template "repo/settings/webhook/base_list" .}}
</div> </div>
{{template "org/settings/layout_footer" .}} {{template "org/settings/layout_footer" .}}

View File

@ -134,7 +134,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="project-column-color-label" for="project-column-color-input">color</label> <label class="project-column-color-label" for="project-column-color-input">color</label>
<div class="js-color-picker-input column"> <div class="color-picker-combo" data-global-init="initColorPicker">
<input maxlength="7" placeholder="#c320f6" id="project-column-color-input" name="color"> <input maxlength="7" placeholder="#c320f6" id="project-column-color-input" name="color">
{{template "repo/issue/label_precolors"}} {{template "repo/issue/label_precolors"}}
</div> </div>

View File

@ -1,22 +1,27 @@
<div class="precolors"> <div class="precolors">
<div class="tw-flex"> <button type="button" class="ui button generate-random-color">
<a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a> {{svg "octicon-sync"}}
<a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a> </button>
<a class="color" style="background-color:#fbca04" data-color-hex="#fbca04"></a> <div>
<a class="color" style="background-color:#009800" data-color-hex="#009800"></a> <div class="tw-flex">
<a class="color" style="background-color:#006b75" data-color-hex="#006b75"></a> <a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a>
<a class="color" style="background-color:#207de5" data-color-hex="#207de5"></a> <a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a>
<a class="color" style="background-color:#0052cc" data-color-hex="#0052cc"></a> <a class="color" style="background-color:#fbca04" data-color-hex="#fbca04"></a>
<a class="color" style="background-color:#5319e7" data-color-hex="#5319e7"></a> <a class="color" style="background-color:#009800" data-color-hex="#009800"></a>
</div> <a class="color" style="background-color:#006b75" data-color-hex="#006b75"></a>
<div class="tw-flex"> <a class="color" style="background-color:#207de5" data-color-hex="#207de5"></a>
<a class="color" style="background-color:#f6c6c7" data-color-hex="#f6c6c7"></a> <a class="color" style="background-color:#0052cc" data-color-hex="#0052cc"></a>
<a class="color" style="background-color:#fad8c7" data-color-hex="#fad8c7"></a> <a class="color" style="background-color:#5319e7" data-color-hex="#5319e7"></a>
<a class="color" style="background-color:#fef2c0" data-color-hex="#fef2c0"></a> </div>
<a class="color" style="background-color:#bfe5bf" data-color-hex="#bfe5bf"></a> <div class="tw-flex">
<a class="color" style="background-color:#bfdadc" data-color-hex="#bfdadc"></a> <a class="color" style="background-color:#f6c6c7" data-color-hex="#f6c6c7"></a>
<a class="color" style="background-color:#c7def8" data-color-hex="#c7def8"></a> <a class="color" style="background-color:#fad8c7" data-color-hex="#fad8c7"></a>
<a class="color" style="background-color:#bfd4f2" data-color-hex="#bfd4f2"></a> <a class="color" style="background-color:#fef2c0" data-color-hex="#fef2c0"></a>
<a class="color" style="background-color:#d4c5f9" data-color-hex="#d4c5f9"></a> <a class="color" style="background-color:#bfe5bf" data-color-hex="#bfe5bf"></a>
<a class="color" style="background-color:#bfdadc" data-color-hex="#bfdadc"></a>
<a class="color" style="background-color:#c7def8" data-color-hex="#c7def8"></a>
<a class="color" style="background-color:#bfd4f2" data-color-hex="#bfd4f2"></a>
<a class="color" style="background-color:#d4c5f9" data-color-hex="#d4c5f9"></a>
</div>
</div> </div>
</div> </div>

View File

@ -49,7 +49,7 @@
</div> </div>
<div class="field"> <div class="field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> <label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="column js-color-picker-input"> <div class="color-picker-combo" data-global-init="initColorPicker">
<!-- the "#" is optional because backend NormalizeColor is able to handle it, API also accepts both formats, and it is easier for users to directly copy-paste a hex value --> <!-- the "#" is optional because backend NormalizeColor is able to handle it, API also accepts both formats, and it is easier for users to directly copy-paste a hex value -->
<input name="color" value="#70c24a" placeholder="#c320f6" required pattern="^#?([\dA-Fa-f]{3}|[\dA-Fa-f]{6})$" maxlength="7"> <input name="color" value="#70c24a" placeholder="#c320f6" required pattern="^#?([\dA-Fa-f]{3}|[\dA-Fa-f]{6})$" maxlength="7">
{{template "repo/issue/label_precolors"}} {{template "repo/issue/label_precolors"}}

View File

@ -1,5 +1,5 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings webhooks")}} {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings webhooks")}}
<div class="repo-setting-content"> <div class="repo-setting-content">
{{template "repo/settings/webhook/list" .}} {{template "repo/settings/webhook/base_list" .}}
</div> </div>
{{template "repo/settings/layout_footer" .}} {{template "repo/settings/layout_footer" .}}

View File

@ -17,7 +17,10 @@
<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a> <a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
</div> </div>
<a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a> <a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
<a class="delete-button tw-p-2" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a> <a class="text red tw-p-2 link-action"
data-url="{{$.Link}}/delete?id={{.ID}}"
data-modal-confirm="{{ctx.Locale.Tr "repo.settings.webhook_deletion_desc"}}"
>{{svg "octicon-trash"}}</a>
</div> </div>
{{end}} {{end}}
</div> </div>

View File

@ -1,10 +0,0 @@
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.settings.webhook_deletion"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.settings.webhook_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>

View File

@ -6,6 +6,7 @@
<label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label> <label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> <input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -14,6 +14,7 @@
<label for="icon_url">{{ctx.Locale.Tr "repo.settings.discord_icon_url"}}</label> <label for="icon_url">{{ctx.Locale.Tr "repo.settings.discord_icon_url"}}</label>
<input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="https://example.com/assets/img/logo.svg"> <input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="https://example.com/assets/img/logo.svg">
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -1,12 +1,14 @@
{{if eq .HookType "feishu"}} {{if eq .HookType "feishu"}}
<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}</p> <p>
<p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}</p> {{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}
{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}
</p>
<form class="ui form" action="{{.BaseLink}}/feishu/{{or .Webhook.ID "new"}}" method="post"> <form class="ui form" action="{{.BaseLink}}/feishu/{{or .Webhook.ID "new"}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="required field {{if .Err_PayloadURL}}error{{end}}"> <div class="required field {{if .Err_PayloadURL}}error{{end}}">
<label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label> <label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> <input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseRequestSecret" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -31,10 +31,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="field {{if .Err_Secret}}error{{end}}"> {{template "repo/settings/webhook/settings" dict
<label for="secret">{{ctx.Locale.Tr "repo.settings.secret"}}</label> "BaseLink" .BaseLink
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off"> "Webhook" .Webhook
</div> "UseAuthorizationHeader" "optional"
{{template "repo/settings/webhook/settings" .}} "UseRequestSecret" "optional"
}}
</form> </form>
{{end}} {{end}}

View File

@ -19,10 +19,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="field {{if .Err_Secret}}error{{end}}"> {{template "repo/settings/webhook/settings" dict
<label for="secret">{{ctx.Locale.Tr "repo.settings.secret"}}</label> "BaseLink" .BaseLink
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off"> "Webhook" .Webhook
</div> "UseAuthorizationHeader" "optional"
{{template "repo/settings/webhook/settings" .}} "UseRequestSecret" "optional"
}}
</form> </form>
{{end}} {{end}}

View File

@ -1,4 +0,0 @@
{{template "repo/settings/webhook/base_list" .}}
{{template "repo/settings/webhook/delete_modal" .}}

View File

@ -22,6 +22,6 @@
</div> </div>
</div> </div>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "required"}}
</form> </form>
{{end}} {{end}}

View File

@ -6,6 +6,7 @@
<label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label> <label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> <input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -14,6 +14,7 @@
<label for="package_url">{{ctx.Locale.Tr "repo.settings.packagist_package_url"}}</label> <label for="package_url">{{ctx.Locale.Tr "repo.settings.packagist_package_url"}}</label>
<input id="package_url" name="package_url" value="{{.PackagistHook.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required> <input id="package_url" name="package_url" value="{{.PackagistHook.PackageURL}}" placeholder="https://packagist.org/packages/laravel/framework" required>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -1,4 +1,52 @@
{{$isNew:=or .PageIsSettingsHooksNew .PageIsAdminDefaultHooksNew .PageIsAdminSystemHooksNew}} {{/* Template attributes:
- BaseLink: Base URL for the repository settings
- WebHook: Webhook object containing details about the webhook
- UseAuthorizationHeader: optional or required
- UseRequestSecret: optional or required
*/}}
{{$isNew := not .Webhook.ID}}
<div class="inline field">
<div class="ui checkbox">
<input name="active" type="checkbox" {{if or $isNew .Webhook.IsActive}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.active"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.active_helper"}}</span>
</div>
</div>
<!-- Authorization Header -->
{{if .UseAuthorizationHeader}}
{{$attributeValid := or (eq .UseAuthorizationHeader "optional") (eq .UseAuthorizationHeader "required")}}
{{if not $attributeValid}}<div class="ui error message">Invalid UseAuthorizationHeader: {{.UseAuthorizationHeader}}}</div>{{end}}
{{$required := eq .UseAuthorizationHeader "required"}}
<div class="field {{if $required}}required{{end}}">
<label>{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
<input name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}" {{if $required}}required placeholder="Bearer $access_token"{{end}}>
{{if not $required}}
<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" (HTMLFormat "<code>%s</code>, <code>%s</code>" "Bearer token123456" "Basic YWxhZGRpbjpvcGVuc2VzYW1l")}}</span>
{{end}}
</div>
{{end}}
<!-- Secret -->
{{if .UseRequestSecret}}
{{$attributeValid := or (eq .UseRequestSecret "optional") (eq .UseRequestSecret "required")}}
{{if not $attributeValid}}<div class="ui error message">Invalid UseRequestSecret: {{.UseRequestSecret}}}</div>{{end}}
{{$required := eq .UseRequestSecret "required"}}
<div class="field {{if $required}}required{{end}}">
<label>{{ctx.Locale.Tr "repo.settings.secret"}}</label>
<input name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off" {{if $required}}required{{end}}>
<span class="help">{{ctx.Locale.Tr "repo.settings.webhook_secret_desc"}}</span>
</div>
{{end}}
<!-- Branch filter -->
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
<input name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}</span>
</div>
<div class="field"> <div class="field">
<h4>{{ctx.Locale.Tr "repo.settings.event_desc"}}</h4> <h4>{{ctx.Locale.Tr "repo.settings.event_desc"}}</h4>
<div class="grouped event type fields"> <div class="grouped event type fields">
@ -286,38 +334,14 @@
</div> </div>
</div> </div>
<!-- Branch filter -->
<div class="field">
<label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
<input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}</span>
</div>
<!-- Authorization Header -->
<div class="field{{if eq .HookType "matrix"}} required{{end}}">
<label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label>
<input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}>
{{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}}
<span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" (HTMLFormat "<code>%s</code>, <code>%s</code>" "Bearer token123456" "Basic YWxhZGRpbjpvcGVuc2VzYW1l")}}</span>
{{end}}
</div>
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox">
<input name="active" type="checkbox" {{if or $isNew .Webhook.IsActive}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.active"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.active_helper"}}</span>
</div>
</div>
<div class="field"> <div class="field">
{{if $isNew}} {{if $isNew}}
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</button>
{{else}} {{else}}
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_webhook"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.settings.update_webhook"}}</button>
<a class="ui red delete-button button" data-url="{{.BaseLink}}/delete" data-id="{{.Webhook.ID}}">{{ctx.Locale.Tr "repo.settings.delete_webhook"}}</a> <a class="ui red button link-action"
data-url="{{.BaseLink}}/delete?id={{.Webhook.ID}}"
data-modal-confirm="{{ctx.Locale.Tr "repo.settings.webhook_deletion_desc"}}"
>{{ctx.Locale.Tr "repo.settings.delete_webhook"}}</a>
{{end}} {{end}}
</div> </div>
{{template "repo/settings/webhook/delete_modal" .}}

View File

@ -23,6 +23,7 @@
<label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label> <label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label>
<input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="#dd4b39, good, warning, danger"> <input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="#dd4b39, good, warning, danger">
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -14,6 +14,7 @@
<label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label> <label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label>
<input id="thread_id" name="thread_id" type="text" value="{{.TelegramHook.ThreadID}}"> <input id="thread_id" name="thread_id" type="text" value="{{.TelegramHook.ThreadID}}">
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -6,6 +6,7 @@
<label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label> <label for="payload_url">{{ctx.Locale.Tr "repo.settings.payload_url"}}</label>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> <input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</div> </div>
{{template "repo/settings/webhook/settings" .}} {{/* FIXME: support authorization header or not? */}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseAuthorizationHeader" "optional"}}
</form> </form>
{{end}} {{end}}

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"version": "{{AppVer | JSEscape}}" "version": "{{.SwaggerAppVer}}"
}, },
"basePath": "{{AppSubUrl | JSEscape}}/api/v1" "basePath": "{{.SwaggerAppSubUrl}}/api/v1"
} }

View File

@ -19,9 +19,9 @@
"name": "MIT", "name": "MIT",
"url": "http://opensource.org/licenses/MIT" "url": "http://opensource.org/licenses/MIT"
}, },
"version": "{{AppVer | JSEscape}}" "version": "{{.SwaggerAppVer}}"
}, },
"basePath": "{{AppSubUrl | JSEscape}}/api/v1", "basePath": "{{.SwaggerAppSubUrl}}/api/v1",
"paths": { "paths": {
"/activitypub/user-id/{user-id}": { "/activitypub/user-id/{user-id}": {
"get": { "get": {

View File

@ -1,16 +1,16 @@
{ {
"issuer": "{{AppUrl | JSEscape}}", "issuer": "{{.OidcIssuer}}",
"authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize", "authorization_endpoint": "{{.OidcBaseUrl}}/login/oauth/authorize",
"token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token", "token_endpoint": "{{.OidcBaseUrl}}/login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys", "jwks_uri": "{{.OidcBaseUrl}}/login/oauth/keys",
"userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo", "userinfo_endpoint": "{{.OidcBaseUrl}}/login/oauth/userinfo",
"introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect", "introspection_endpoint": "{{.OidcBaseUrl}}/login/oauth/introspect",
"response_types_supported": [ "response_types_supported": [
"code", "code",
"id_token" "id_token"
], ],
"id_token_signing_alg_values_supported": [ "id_token_signing_alg_values_supported": [
"{{.SigningKey.SigningMethod.Alg | JSEscape}}" "{{.SigningKeyMethodAlg}}"
], ],
"subject_types_supported": [ "subject_types_supported": [
"public" "public"

View File

@ -1,5 +1,5 @@
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings webhooks")}} {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings webhooks")}}
<div class="user-setting-content"> <div class="user-setting-content">
{{template "repo/settings/webhook/list" .}} {{template "repo/settings/webhook/base_list" .}}
</div> </div>
{{template "user/settings/layout_footer" .}} {{template "user/settings/layout_footer" .}}

View File

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
@ -26,24 +27,33 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestAuthorizeNoClientID(t *testing.T) { func TestOAuth2Provider(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("OAuth2WellKnown", testOAuth2WellKnown)
}
func testAuthorizeNoClientID(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize") req := NewRequest(t, "GET", "/login/oauth/authorize")
ctx := loginUser(t, "user2") ctx := loginUser(t, "user2")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest) resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Client ID not registered") assert.Contains(t, resp.Body.String(), "Client ID not registered")
} }
func TestAuthorizeUnregisteredRedirect(t *testing.T) { func testAuthorizeUnregisteredRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=UNREGISTERED&response_type=code&state=thestate") req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=UNREGISTERED&response_type=code&state=thestate")
ctx := loginUser(t, "user1") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest) resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI") assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI")
} }
func TestAuthorizeUnsupportedResponseType(t *testing.T) { func testAuthorizeUnsupportedResponseType(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate") req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate")
ctx := loginUser(t, "user1") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
@ -53,8 +63,7 @@ func TestAuthorizeUnsupportedResponseType(t *testing.T) {
assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description")) assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description"))
} }
func TestAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) { func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=UNEXPECTED") req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=UNEXPECTED")
ctx := loginUser(t, "user1") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
@ -64,8 +73,7 @@ func TestAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
assert.Equal(t, "unsupported code challenge method", u.Query().Get("error_description")) assert.Equal(t, "unsupported code challenge method", u.Query().Get("error_description"))
} }
func TestAuthorizeLoginRedirect(t *testing.T) { func testAuthorizeLoginRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize") req := NewRequest(t, "GET", "/login/oauth/authorize")
assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login") assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
} }
@ -903,3 +911,23 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
assert.Contains(t, userinfoParsed.Groups, group) assert.Contains(t, userinfoParsed.Groups, group)
} }
} }
func testOAuth2WellKnown(t *testing.T) {
urlOpenidConfiguration := "/.well-known/openid-configuration"
defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
req := NewRequest(t, "GET", urlOpenidConfiguration)
resp := MakeRequest(t, req, http.StatusOK)
var respMap map[string]any
DecodeJSON(t, resp, &respMap)
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
}

View File

@ -1031,19 +1031,6 @@ table th[data-sortt-desc] .svg {
min-height: 0; min-height: 0;
} }
.precolors {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 1em;
}
.precolors .color {
display: inline-block;
width: 15px;
height: 15px;
}
.ui.dropdown:not(.button) { .ui.dropdown:not(.button) {
line-height: var(--line-height-default); /* the dropdown doesn't have default line-height, use this to make the dropdown icon align with plain dropdown */ line-height: var(--line-height-default); /* the dropdown doesn't have default line-height, use this to make the dropdown icon align with plain dropdown */
} }

View File

@ -1,15 +1,13 @@
.js-color-picker-input { .color-picker-combo {
display: flex; display: flex;
position: relative; position: relative; /* to position the preview square */
} }
.js-color-picker-input input { .color-picker-combo input {
padding-top: 8px !important;
padding-bottom: 8px !important;
padding-left: 32px !important; padding-left: 32px !important;
} }
.js-color-picker-input .preview-square { .color-picker-combo .preview-square {
position: absolute; position: absolute;
aspect-ratio: 1; aspect-ratio: 1;
height: 16px; height: 16px;
@ -22,7 +20,7 @@
background-size: 8px 8px; background-size: 8px 8px;
} }
.js-color-picker-input .preview-square::after { .color-picker-combo .preview-square::after {
content: ""; content: "";
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -31,6 +29,26 @@
background-color: currentcolor; background-color: currentcolor;
} }
.color-picker-combo .precolors {
display: flex;
margin-left: 1em;
align-items: center;
gap: 0.125em;
}
.color-picker-combo .precolors .generate-random-color {
padding: 0;
width: 30px;
height: 30px;
min-height: 0;
}
.color-picker-combo .precolors .color {
display: inline-block;
width: 15px;
height: 15px;
}
hex-color-picker { hex-color-picker {
width: 180px; width: 180px;
height: 120px; height: 120px;

View File

@ -71,7 +71,7 @@
.card-attachment-images { .card-attachment-images {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
overflow: scroll; overflow: auto;
cursor: default; cursor: default;
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
text-align: center; text-align: center;
@ -85,6 +85,7 @@
scroll-snap-align: center; scroll-snap-align: center;
margin-right: 2px; margin-right: 2px;
aspect-ratio: 1; aspect-ratio: 1;
object-fit: contain;
} }
.card-attachment-images img:only-child { .card-attachment-images img:only-child {

View File

@ -1,18 +1,19 @@
import {createTippy} from '../modules/tippy.ts'; import {createTippy} from '../modules/tippy.ts';
import type {DOMEvent} from '../utils/dom.ts'; import type {DOMEvent} from '../utils/dom.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
export async function initColorPickers() { export async function initColorPickers() {
const els = document.querySelectorAll<HTMLElement>('.js-color-picker-input'); let imported = false;
if (!els.length) return; registerGlobalInitFunc('initColorPicker', async (el) => {
if (!imported) {
await Promise.all([ await Promise.all([
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'), import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'), import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]); ]);
imported = true;
for (const el of els) { }
initPicker(el); initPicker(el);
} });
} }
function updateSquare(el: HTMLElement, newValue: string): void { function updateSquare(el: HTMLElement, newValue: string): void {
@ -55,13 +56,20 @@ function initPicker(el: HTMLElement): void {
}, },
}); });
// init precolors // init random color & precolors
const setSelectedColor = (color: string) => {
input.value = color;
input.dispatchEvent(new Event('input', {bubbles: true}));
updateSquare(square, color);
};
el.querySelector('.generate-random-color').addEventListener('click', () => {
const newValue = `#${Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')}`;
setSelectedColor(newValue);
});
for (const colorEl of el.querySelectorAll<HTMLElement>('.precolors .color')) { for (const colorEl of el.querySelectorAll<HTMLElement>('.precolors .color')) {
colorEl.addEventListener('click', (e: DOMEvent<MouseEvent, HTMLAnchorElement>) => { colorEl.addEventListener('click', (e: DOMEvent<MouseEvent, HTMLAnchorElement>) => {
const newValue = e.target.getAttribute('data-color-hex'); const newValue = e.target.getAttribute('data-color-hex');
input.value = newValue; setSelectedColor(newValue);
input.dispatchEvent(new Event('input', {bubbles: true}));
updateSquare(square, newValue);
}); });
} }
} }

View File

@ -24,7 +24,7 @@ export function initCompLabelEdit(pageSelector: string) {
const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field'); const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field');
const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-input'); const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-input');
const elDescInput = elModal.querySelector<HTMLInputElement>('.label-desc-input'); const elDescInput = elModal.querySelector<HTMLInputElement>('.label-desc-input');
const elColorInput = elModal.querySelector<HTMLInputElement>('.js-color-picker-input input'); const elColorInput = elModal.querySelector<HTMLInputElement>('.color-picker-combo input');
const syncModalUi = () => { const syncModalUi = () => {
const hasScope = nameHasScope(elNameInput.value); const hasScope = nameHasScope(elNameInput.value);