0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-20 14:48:30 +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-specific: true
gocritic:
enabled-checks:
- equalFold
disabled-checks:
- ifElseChain
- 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.**
- 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 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 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.**
@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta
### 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

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)
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).

View File

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

View File

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

View File

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

View File

@ -40,7 +40,6 @@ func NewFuncMap() template.FuncMap {
"HTMLFormat": htmlFormat,
"QueryEscape": queryEscape,
"QueryBuild": QueryBuild,
"JSEscape": jsEscapeSafe,
"SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin,
"DotEscape": dotEscape,
@ -181,10 +180,6 @@ func htmlFormat(s any, args ...any) template.HTML {
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 {
return template.URL(url.QueryEscape(s))
}

View File

@ -57,10 +57,6 @@ func TestSubjectBodySeparator(t *testing.T) {
"Insufficient\n--\nSeparators")
}
func TestJSEscapeSafe(t *testing.T) {
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, jsEscapeSafe(`&<>'"`))
}
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>`))
}

View File

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

View File

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

View File

@ -2355,6 +2355,7 @@ settings.payload_url = Target URL
settings.http_method = HTTP Method
settings.content_type = POST Content Type
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_icon_url = Icon URL
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.
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
owner = ctx.Doer
} else {
owner, err = user_model.GetUserByName(ctx, userName)

View File

@ -276,7 +276,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
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")
return
}

View File

@ -669,7 +669,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
newRepoName = *opts.Name
}
// 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 {
switch {
case repo_model.IsErrRepoAlreadyExist(err):

View File

@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
@ -161,9 +162,7 @@ func IntrospectOAuth(ctx *context.Context) {
if err == nil && app != nil {
response.Active = true
response.Scope = grant.Scope
response.Issuer = setting.AppURL
response.Audience = []string{app.ClientID}
response.Subject = strconv.FormatInt(grant.UserID, 10)
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/)
}
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
response.Username = user.Name
@ -423,7 +422,14 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
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")
}

View File

@ -249,7 +249,7 @@ func ViewPost(ctx *context_module.Context) {
ID: v.ID,
Name: v.Name,
Status: v.Status.String(),
CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
CanRerun: resp.State.Run.CanRerun,
Duration: v.Duration().String(),
})
}
@ -445,7 +445,7 @@ func Rerun(ctx *context_module.Context) {
return
}
}
ctx.JSON(http.StatusOK, struct{}{})
ctx.JSONOK()
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 {
status := job.Status
if !status.IsDone() {
if !status.IsDone() || !job.Run.Status.IsDone() {
return nil
}

View File

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

View File

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

View File

@ -4,10 +4,15 @@
package web
import (
"html/template"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
)
// SwaggerV1Json render swagger v1 json
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")
}

View File

@ -241,7 +241,7 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
}
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
if strings.ToLower(source.UserUID) == "dn" {
if strings.EqualFold(source.UserUID, "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
// 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) {
t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
if err != nil {

View File

@ -208,7 +208,7 @@ func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) {
if len(teamName) > 0 {
teamExists := false
for _, team := range ctx.Org.Teams {
if team.LowerName == strings.ToLower(teamName) {
if strings.EqualFold(team.LowerName, teamName) {
teamExists = true
ctx.Org.Team = team
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
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) {
ctx.Repo.Owner = ctx.Doer
} else {
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) {
username := ctx.PathParam("username")
if doer != nil && doer.LowerName == strings.ToLower(username) {
if doer != nil && strings.EqualFold(doer.LowerName, username) {
contextUser = doer
} else {
var err error

View File

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

View File

@ -106,6 +106,20 @@ func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
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) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil {
@ -176,12 +190,7 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
}
idToken := &OIDCToken{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
Issuer: setting.AppURL,
Audience: []string{app.ClientID},
Subject: strconv.FormatInt(grant.UserID, 10),
},
RegisteredClaims: NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, jwt.NewNumericDate(expirationDate.AsTime())),
Nonce: grant.Nonce,
}
if grant.ScopeContains("profile") {

View File

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

View File

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

View File

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

View File

@ -134,7 +134,7 @@
</div>
<div class="field">
<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">
{{template "repo/issue/label_precolors"}}
</div>

View File

@ -1,4 +1,8 @@
<div class="precolors">
<button type="button" class="ui button generate-random-color">
{{svg "octicon-sync"}}
</button>
<div>
<div class="tw-flex">
<a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a>
<a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a>
@ -19,4 +23,5 @@
<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>

View File

@ -49,7 +49,7 @@
</div>
<div class="field">
<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 -->
<input name="color" value="#70c24a" placeholder="#c320f6" required pattern="^#?([\dA-Fa-f]{3}|[\dA-Fa-f]{6})$" maxlength="7">
{{template "repo/issue/label_precolors"}}

View File

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

View File

@ -17,7 +17,10 @@
<a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a>
</div>
<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>
{{end}}
</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>
<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required>
</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>
{{end}}

View File

@ -14,6 +14,7 @@
<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">
</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>
{{end}}

View File

@ -1,12 +1,14 @@
{{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>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}</p>
<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">
{{.CsrfTokenHtml}}
<div class="required field {{if .Err_PayloadURL}}error{{end}}">
<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>
</div>
{{template "repo/settings/webhook/settings" .}}
{{template "repo/settings/webhook/settings" dict "BaseLink" .BaseLink "Webhook" .Webhook "UseRequestSecret" "optional"}}
</form>
{{end}}

View File

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

View File

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

View File

@ -6,6 +6,7 @@
<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>
</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>
{{end}}

View File

@ -14,6 +14,7 @@
<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>
</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>
{{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">
<h4>{{ctx.Locale.Tr "repo.settings.event_desc"}}</h4>
<div class="grouped event type fields">
@ -286,38 +334,14 @@
</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">
{{if $isNew}}
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</button>
{{else}}
<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}}
</div>
{{template "repo/settings/webhook/delete_modal" .}}

View File

@ -23,6 +23,7 @@
<label for="color">{{ctx.Locale.Tr "repo.settings.slack_color"}}</label>
<input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="#dd4b39, good, warning, danger">
</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>
{{end}}

View File

@ -14,6 +14,7 @@
<label for="thread_id">{{ctx.Locale.Tr "repo.settings.thread_id"}}</label>
<input id="thread_id" name="thread_id" type="text" value="{{.TelegramHook.ThreadID}}">
</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>
{{end}}

View File

@ -6,6 +6,7 @@
<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>
</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>
{{end}}

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests"
@ -26,24 +27,33 @@ import (
"github.com/stretchr/testify/require"
)
func TestAuthorizeNoClientID(t *testing.T) {
func TestOAuth2Provider(t *testing.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")
ctx := loginUser(t, "user2")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Client ID not registered")
}
func TestAuthorizeUnregisteredRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizeUnregisteredRedirect(t *testing.T) {
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")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI")
}
func TestAuthorizeUnsupportedResponseType(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizeUnsupportedResponseType(t *testing.T) {
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")
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"))
}
func TestAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.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")
ctx := loginUser(t, "user1")
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"))
}
func TestAuthorizeLoginRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizeLoginRedirect(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize")
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)
}
}
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;
}
.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) {
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;
position: relative;
position: relative; /* to position the preview square */
}
.js-color-picker-input input {
padding-top: 8px !important;
padding-bottom: 8px !important;
.color-picker-combo input {
padding-left: 32px !important;
}
.js-color-picker-input .preview-square {
.color-picker-combo .preview-square {
position: absolute;
aspect-ratio: 1;
height: 16px;
@ -22,7 +20,7 @@
background-size: 8px 8px;
}
.js-color-picker-input .preview-square::after {
.color-picker-combo .preview-square::after {
content: "";
position: absolute;
width: 100%;
@ -31,6 +29,26 @@
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 {
width: 180px;
height: 120px;

View File

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

View File

@ -1,18 +1,19 @@
import {createTippy} from '../modules/tippy.ts';
import type {DOMEvent} from '../utils/dom.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';
export async function initColorPickers() {
const els = document.querySelectorAll<HTMLElement>('.js-color-picker-input');
if (!els.length) return;
let imported = false;
registerGlobalInitFunc('initColorPicker', async (el) => {
if (!imported) {
await Promise.all([
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]);
for (const el of els) {
initPicker(el);
imported = true;
}
initPicker(el);
});
}
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')) {
colorEl.addEventListener('click', (e: DOMEvent<MouseEvent, HTMLAnchorElement>) => {
const newValue = e.target.getAttribute('data-color-hex');
input.value = newValue;
input.dispatchEvent(new Event('input', {bubbles: true}));
updateSquare(square, newValue);
setSelectedColor(newValue);
});
}
}

View File

@ -24,7 +24,7 @@ export function initCompLabelEdit(pageSelector: string) {
const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field');
const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-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 hasScope = nameHasScope(elNameInput.value);