Compare commits

...

10 Commits

Author SHA1 Message Date
GiteaBot
de1a5506d1 [skip ci] Updated translations via Crowdin 2024-07-17 00:27:54 +00:00
Carsten Klein
3571b7e3dd
Allow searching issues by ID (#31479)
When you are entering a number in the issue search, you likely want the
issue with the given ID (code internal concept: issue index).
As such, when a number is detected, the issue with the corresponding ID
will now be added to the results.

Fixes #4479

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-07-17 00:49:05 +02:00
Rowan Bohde
416c36f303
allow synchronizing user status from OAuth2 login providers (#31572)
This leverages the existing `sync_external_users` cron job to
synchronize the `IsActive` flag on users who use an OAuth2 provider set
to synchronize. This synchronization is done by checking for expired
access tokens, and using the stored refresh token to request a new
access token. If the response back from the OAuth2 provider is the
`invalid_grant` error code, the user is marked as inactive. However, the
user is able to reactivate their account by logging in the web browser
through their OAuth2 flow.

Also changed to support this is that a linked `ExternalLoginUser` is
always created upon a login or signup via OAuth2.

### Notes on updating permissions
Ideally, we would also refresh permissions from the configured OAuth
provider (e.g., admin, restricted and group mappings) to match the
implementation of LDAP. However, the OAuth library used for this `goth`,
doesn't seem to support issuing a session via refresh tokens. The
interface provides a [`RefreshToken`
method](https://github.com/markbates/goth/blob/master/provider.go#L20),
but the returned `oauth.Token` doesn't implement the `goth.Session` we
would need to call `FetchUser`. Due to specific implementations, we
would need to build a compatibility function for every provider, since
they cast to concrete types (e.g.
[Azure](https://github.com/markbates/goth/blob/master/providers/azureadv2/azureadv2.go#L132))

---------

Co-authored-by: Kyle D <kdumontnu@gmail.com>
2024-07-16 20:33:16 +02:00
3a7454df7a
Enable no-jquery/no-class-state (#31639)
Just 4 validations and I specifically tested this by
selecting/unselecting issue labels.

Co-authored-by: Giteabot <teabot@gitea.io>
2024-07-16 12:14:27 +02:00
Bartlomiej Komendarczuk
e8d4b7a8b1
Added default sorting milestones by name (#27084)
#26996 
Added default sorting for milestones by name.
Additional, name for sorting closestduedate and furthestduedate was
broken, so I fixed it.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-07-16 10:08:54 +02:00
0bb4c1cde2
Code editor theme enhancements (#31629)
1. Fixed border-radius
2. Monaco ignores the alpha channel on the shadow color, introduce
`color-shadow-opaque`
3. Remove scrollbar color which follows
https://github.com/go-gitea/gitea/pull/29800

Before:

<img width="34" alt="Screenshot 2024-07-13 at 15 38 18"
src="https://github.com/user-attachments/assets/042d9bde-6db9-4467-a2a4-8f61ecc773eb">
<img width="35" alt="Screenshot 2024-07-13 at 15 38 31"
src="https://github.com/user-attachments/assets/04146ee0-551c-4ff2-9636-bd119b33595a">


After:

<img width="45" alt="Screenshot 2024-07-13 at 15 38 06"
src="https://github.com/user-attachments/assets/1f58fa5a-1289-4e45-83c9-18ca82a5e266">
<img width="39" alt="Screenshot 2024-07-13 at 21 16 56"
src="https://github.com/user-attachments/assets/e12ebe22-b29b-4798-9f0d-4c100f311562">
2024-07-14 23:22:48 +00:00
6543
0f53324182
Add option to change mail from user display name (#31528)
Make it posible to let mails show e.g.:

`Max Musternam (via gitea.kithara.com) <gitea@kithara.com>`

Docs: https://gitea.com/gitea/docs/pulls/23

---
*Sponsored by Kithara Software GmbH*
2024-07-14 23:27:00 +02:00
0d08bb6112
Upgrade xorm to v1.3.9 and improve some migrations Sync (#29899)
Co-authored-by: 6543 <6543@obermui.de>
2024-07-14 17:15:59 -04:00
6543
1064e817c4
Issue Templates: add option to have dropdown printed list (#31577)
Issue template dropdown can have many entries, and it could be better to
have them rendered as list later on if multi-select is enabled.

so this adds an option to the issue template engine to do so.

DOCS: https://gitea.com/gitea/docs/pulls/19

---

## demo:

```yaml
name: Name
title: Title
about: About
labels: ["label1", "label2"]
ref: Ref
body:
  - type: dropdown
    id: id6
    attributes:
      label: Label of dropdown (list)
      description: Description of dropdown
      multiple: true
      list: true
      options:
        - Option 1 of dropdown
        - Option 2 of dropdown
        - Option 3 of dropdown
        - Option 4 of dropdown
        - Option 5 of dropdown
        - Option 6 of dropdown
        - Option 7 of dropdown
        - Option 8 of dropdown
        - Option 9 of dropdown
```


![image](https://github.com/user-attachments/assets/102ed0f4-89da-420b-ab2a-1788b59676f9)

![image](https://github.com/user-attachments/assets/a2bdb14e-43ff-4cc6-9bbe-20244830453c)


---
*Sponsored by Kithara Software GmbH*
2024-07-14 16:38:45 +02:00
6543
957c75bfd2
Fix update flake (#31626) 2024-07-12 16:25:54 +00:00
73 changed files with 643 additions and 93 deletions

@ -512,7 +512,7 @@ rules:
no-jquery/no-box-model: [2]
no-jquery/no-browser: [2]
no-jquery/no-camel-case: [2]
no-jquery/no-class-state: [0]
no-jquery/no-class-state: [2]
no-jquery/no-class: [0]
no-jquery/no-clone: [2]
no-jquery/no-closest: [0]

@ -1676,6 +1676,10 @@ LEVEL = Info
;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address.
;ENVELOPE_FROM =
;;
;; If gitea sends mails on behave of users, it will just use the name also displayed in the WebUI. If you want e.g. `Mister X (by CodeIt) <gitea@codeit.net>`,
;; set it to `{{ .DisplayName }} (by {{ .AppName }})`. Available Variables: `.DisplayName`, `.AppName` and `.Domain`.
;FROM_DISPLAY_NAME_FORMAT = {{ .DisplayName }}
;;
;; Mailer user name and password, if required by provider.
;USER =
;;

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1717974879,
"narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
"lastModified": 1720542800,
"narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
"rev": "feb2849fdeb70028c70d73b848214b00d324a497",
"type": "github"
},
"original": {

@ -31,6 +31,7 @@
# backend
go_1_22
gofumpt
sqlite
];
};
}

2
go.mod

@ -123,7 +123,7 @@ require (
mvdan.cc/xurls/v2 v2.5.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.8
xorm.io/xorm v1.3.9
)
require (

4
go.sum

@ -1064,5 +1064,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=

@ -210,7 +210,7 @@ func CreateSource(ctx context.Context, source *Source) error {
return ErrSourceAlreadyExist{source.Name}
}
// Synchronization is only available with LDAP for now
if !source.IsLDAP() {
if !source.IsLDAP() && !source.IsOAuth2() {
source.IsSyncEnabled = false
}

@ -70,8 +70,10 @@ func (opts FindMilestoneOptions) ToOrders() string {
return "num_issues DESC"
case "id":
return "id ASC"
case "name":
return "name DESC"
default:
return "deadline_unix ASC, id ASC"
return "deadline_unix ASC, name ASC"
}
}

@ -12,5 +12,9 @@ func AddIndexToActionUserID(x *xorm.Engine) error {
UserID int64 `xorm:"INDEX"`
}
return x.Sync(new(Action))
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, new(Action))
return err
}

@ -10,5 +10,9 @@ func AddIgnoreStaleApprovalsColumnToProtectedBranchTable(x *xorm.Engine) error {
type ProtectedBranch struct {
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(ProtectedBranch))
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(ProtectedBranch))
return err
}

@ -14,5 +14,9 @@ func AddPreviousDurationToActionRun(x *xorm.Engine) error {
PreviousDuration time.Duration
}
return x.Sync(&ActionRun{})
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &ActionRun{})
return err
}

@ -86,7 +86,10 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
}
if err := x.Sync(new(Repository)); err != nil {
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(Repository)); err != nil {
return err
}

@ -10,7 +10,10 @@ func AddDefaultWikiBranch(x *xorm.Engine) error {
ID int64
DefaultWikiBranch string
}
if err := x.Sync(&Repository{}); err != nil {
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, &Repository{}); err != nil {
return err
}
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")

@ -13,5 +13,12 @@ type HookTask struct {
func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
// create missing column
return x.Sync(new(HookTask))
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreIndices: true,
IgnoreConstrains: true,
}, new(HookTask)); err != nil {
return err
}
_, err := x.Exec("UPDATE hook_task SET payload_version = 1 WHERE payload_version IS NULL")
return err
}

@ -10,5 +10,9 @@ func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
CommentID int64 `xorm:"INDEX"`
}
return x.Sync(&Attachment{})
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
IgnoreConstrains: true,
}, &Attachment{})
return err
}

@ -160,12 +160,34 @@ func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLogin
return err
}
// EnsureLinkExternalToUser link the external user to the user
func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error {
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
"external_id": external.ExternalID,
"login_source_id": external.LoginSourceID,
})
if err != nil {
return err
}
if has {
_, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external)
return err
}
_, err = db.GetEngine(ctx).Insert(external)
return err
}
// FindExternalUserOptions represents an options to find external users
type FindExternalUserOptions struct {
db.ListOptions
Provider string
UserID int64
OrderBy string
Provider string
UserID int64
LoginSourceID int64
HasRefreshToken bool
Expired bool
OrderBy string
}
func (opts FindExternalUserOptions) ToConds() builder.Cond {
@ -176,9 +198,22 @@ func (opts FindExternalUserOptions) ToConds() builder.Cond {
if opts.UserID > 0 {
cond = cond.And(builder.Eq{"user_id": opts.UserID})
}
if opts.Expired {
cond = cond.And(builder.Lt{"expires_at": time.Now()})
}
if opts.HasRefreshToken {
cond = cond.And(builder.Neq{"refresh_token": ""})
}
if opts.LoginSourceID != 0 {
cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
}
return cond
}
func (opts FindExternalUserOptions) ToOrders() string {
return opts.OrderBy
}
func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error {
return db.Iterate(ctx, opts.ToConds(), f)
}

@ -71,6 +71,12 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
)),
),
)
if options.IsKeywordNumeric() {
cond = cond.Or(
builder.Eq{"`index`": options.Keyword},
)
}
}
opt, err := ToDBOptions(ctx, options)

@ -283,9 +283,9 @@ const (
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
indexer := *globalIndexer.Load()
if opts.Keyword == "" {
if opts.Keyword == "" || opts.IsKeywordNumeric() {
// This is a conservative shortcut.
// If the keyword is empty, db has better (at least not worse) performance to filter issues.
// If the keyword is empty or an integer, db has better (at least not worse) performance to filter issues.
// When the keyword is empty, it tends to listing rather than searching issues.
// So if the user creates an issue and list issues immediately, the issue may not be listed because the indexer needs time to index the issue.
// Even worse, the external indexer like elastic search may not be available for a while,

@ -31,6 +31,7 @@ func TestDBSearchIssues(t *testing.T) {
InitIssueIndexer(true)
t.Run("search issues with keyword", searchIssueWithKeyword)
t.Run("search issues by index", searchIssueByIndex)
t.Run("search issues in repo", searchIssueInRepo)
t.Run("search issues by ID", searchIssueByID)
t.Run("search issues is pr", searchIssueIsPull)
@ -87,6 +88,43 @@ func searchIssueWithKeyword(t *testing.T) {
}
}
func searchIssueByIndex(t *testing.T) {
tests := []struct {
opts SearchOptions
expectedIDs []int64
}{
{
SearchOptions{
Keyword: "1000",
RepoIDs: []int64{1},
},
[]int64{},
},
{
SearchOptions{
Keyword: "2",
RepoIDs: []int64{1, 2, 3, 32},
},
[]int64{17, 12, 7, 2},
},
{
SearchOptions{
Keyword: "1",
RepoIDs: []int64{58},
},
[]int64{19},
},
}
for _, test := range tests {
issueIDs, _, err := SearchIssues(context.TODO(), &test.opts)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, test.expectedIDs, issueIDs)
}
}
func searchIssueInRepo(t *testing.T) {
tests := []struct {
opts SearchOptions

@ -4,6 +4,8 @@
package internal
import (
"strconv"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/timeutil"
@ -124,6 +126,12 @@ func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOption
return &v
}
// used for optimized issue index based search
func (o *SearchOptions) IsKeywordNumeric() bool {
_, err := strconv.Atoi(o.Keyword)
return err == nil
}
type SortBy string
const (

@ -88,6 +88,9 @@ func validateYaml(template *api.IssueTemplate) error {
if err := validateBoolItem(position, field.Attributes, "multiple"); err != nil {
return err
}
if err := validateBoolItem(position, field.Attributes, "list"); err != nil {
return err
}
if err := validateOptions(field, idx); err != nil {
return err
}
@ -340,7 +343,13 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
}
}
if len(checkeds) > 0 {
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
if list, ok := f.Attributes["list"].(bool); ok && list {
for _, check := range checkeds {
_, _ = fmt.Fprintf(builder, "- %s\n", check)
}
} else {
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
}
} else {
_, _ = fmt.Fprint(builder, blankPlaceholder)
}

@ -216,6 +216,20 @@ body:
`,
wantErr: "body[0](dropdown): 'multiple' should be a bool",
},
{
name: "dropdown invalid list",
content: `
name: "test"
about: "this is about"
body:
- type: "dropdown"
id: "1"
attributes:
label: "a"
list: "on"
`,
wantErr: "body[0](dropdown): 'list' should be a bool",
},
{
name: "checkboxes invalid description",
content: `
@ -807,7 +821,7 @@ body:
- type: dropdown
id: id5
attributes:
label: Label of dropdown
label: Label of dropdown (one line)
description: Description of dropdown
multiple: true
options:
@ -816,8 +830,21 @@ body:
- Option 3 of dropdown
validations:
required: true
- type: checkboxes
- type: dropdown
id: id6
attributes:
label: Label of dropdown (list)
description: Description of dropdown
multiple: true
list: true
options:
- Option 1 of dropdown
- Option 2 of dropdown
- Option 3 of dropdown
validations:
required: true
- type: checkboxes
id: id7
attributes:
label: Label of checkboxes
description: Description of checkboxes
@ -836,8 +863,9 @@ body:
"form-field-id3": {"Value of id3"},
"form-field-id4": {"Value of id4"},
"form-field-id5": {"0,1"},
"form-field-id6-0": {"on"},
"form-field-id6-2": {"on"},
"form-field-id6": {"1,2"},
"form-field-id7-0": {"on"},
"form-field-id7-2": {"on"},
},
},
@ -849,10 +877,15 @@ body:
Value of id4
### Label of dropdown
### Label of dropdown (one line)
Option 1 of dropdown, Option 2 of dropdown
### Label of dropdown (list)
- Option 2 of dropdown
- Option 3 of dropdown
### Label of checkboxes
- [x] Option 1 of checkboxes

@ -8,6 +8,7 @@ import (
"net"
"net/mail"
"strings"
"text/template"
"time"
"code.gitea.io/gitea/modules/log"
@ -46,6 +47,10 @@ type Mailer struct {
SendmailArgs []string `ini:"-"`
SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
// Customization
FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
FromDisplayNameFormatTemplate *template.Template `ini:"-"`
}
// MailService the global mailer
@ -226,6 +231,16 @@ func loadMailerFrom(rootCfg ConfigProvider) {
log.Error("no mailer.FROM provided, email system may not work.")
}
MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
if MailService.FromDisplayNameFormat != "" {
template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
if err != nil {
log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
} else {
MailService.FromDisplayNameFormatTemplate = template
}
}
switch MailService.EnvelopeFrom {
case "":
MailService.OverrideEnvelopeFrom = false

@ -1927,6 +1927,7 @@ milestones.edit_success=Milník „%s“ byl aktualizován.
milestones.deletion=Smazat milník
milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat?
milestones.deletion_success=Milník byl odstraněn.
milestones.filter_sort.name=Název
milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení
milestones.filter_sort.latest_due_date=Nejnovější datum dokončení
milestones.filter_sort.least_complete=Nejméně dokončené

@ -1907,6 +1907,7 @@ milestones.edit_success=Meilenstein "%s" wurde aktualisiert.
milestones.deletion=Meilenstein löschen
milestones.deletion_desc=Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren?
milestones.deletion_success=Der Meilenstein wurde gelöscht.
milestones.filter_sort.name=Name
milestones.filter_sort.earliest_due_data=Frühestes Fälligkeitsdatum
milestones.filter_sort.latest_due_date=Spätestes Fälligkeitsdatum
milestones.filter_sort.least_complete=Am wenigsten vollständig

@ -1826,6 +1826,7 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε.
milestones.deletion=Διαγραφή Ορόσημου
milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια;
milestones.deletion_success=Το ορόσημο έχει διαγραφεί.
milestones.filter_sort.name=Όνομα
milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση
milestones.filter_sort.latest_due_date=Απώτερη παράδοση
milestones.filter_sort.least_complete=Λιγότερο πλήρη

@ -1935,6 +1935,7 @@ milestones.edit_success = Milestone "%s" has been updated.
milestones.deletion = Delete Milestone
milestones.deletion_desc = Deleting a milestone removes it from all related issues. Continue?
milestones.deletion_success = The milestone has been deleted.
milestones.filter_sort.name = Name
milestones.filter_sort.earliest_due_data = Earliest due date
milestones.filter_sort.latest_due_date = Latest due date
milestones.filter_sort.least_complete = Least complete

@ -1812,6 +1812,7 @@ milestones.edit_success=Se ha actualizado el hito "%s".
milestones.deletion=Eliminar hito
milestones.deletion_desc=Eliminando un hito lo elimina de todos los problemas relacionados. ¿Continuar?
milestones.deletion_success=El hito se ha eliminado.
milestones.filter_sort.name=Nombre
milestones.filter_sort.earliest_due_data=Fecha de vencimiento más temprana
milestones.filter_sort.latest_due_date=Fecha de vencimiento más lejana
milestones.filter_sort.least_complete=Menos completa

@ -1389,6 +1389,7 @@ milestones.modify=به روزرسانی نقطه عطف
milestones.deletion=حذف نقطه عطف
milestones.deletion_desc=نقاط عطف از تمام مسائل مرتبط حذف میشوند. آیا ادامه میدهید؟
milestones.deletion_success=نقطه عطف حذف شد.
milestones.filter_sort.name=نام
milestones.filter_sort.least_complete=حداقل کامل شده
milestones.filter_sort.most_complete=بیشترین کامل شده
milestones.filter_sort.most_issues=بیشترین مسائل

@ -1009,6 +1009,7 @@ milestones.clear=Tyhjennä
milestones.edit=Muokkaa merkkipaalua
milestones.cancel=Peruuta
milestones.modify=Päivitä merkkipaalu
milestones.filter_sort.name=Nimi
milestones.filter_sort.most_issues=Eniten ongelmia
milestones.filter_sort.least_issues=Vähiten ongelmia

@ -1927,6 +1927,7 @@ milestones.edit_success=Le jalon "%s" a été mis à jour.
milestones.deletion=Supprimer un Jalon
milestones.deletion_desc=Supprimer un jalon le retire de tous les tickets. Continuer ?
milestones.deletion_success=Le jalon a été supprimé.
milestones.filter_sort.name=Nom
milestones.filter_sort.earliest_due_data=Date déchéance la plus ancienne
milestones.filter_sort.latest_due_date=Date déchéance la plus récente
milestones.filter_sort.least_complete=Le moins complété

@ -962,6 +962,7 @@ milestones.modify=Mérföldkő frissítése
milestones.deletion=Mérföldkő törlése
milestones.deletion_desc=A mérföldkő törlése eltávolítja az összes hozzárendelt hibajegyet. Biztosan folytatja?
milestones.deletion_success=A mérföldkő törölve.
milestones.filter_sort.name=Név
milestones.filter_sort.least_complete=Legkevésbé befejezve
milestones.filter_sort.most_complete=Leginkább befejezve
milestones.filter_sort.most_issues=Legtöbb hibajegy

@ -770,6 +770,7 @@ milestones.due_date=Jatuh Tempo (opsional)
milestones.clear=Bersihkan
milestones.edit=Ubah Milestone
milestones.cancel=Batal
milestones.filter_sort.name=Nama
milestones.filter_sort.least_complete=Paling tidak lengkap
milestones.filter_sort.most_complete=Paling lengkap
milestones.filter_sort.most_issues=Paling banyak masalah

@ -916,6 +916,7 @@ milestones.desc=Lýsing
milestones.due_date=Eindagi (valfrjálst)
milestones.clear=Hreinsa
milestones.cancel=Hætta við
milestones.filter_sort.name=Heiti
milestones.filter_sort.most_issues=Flest vandamál
milestones.filter_sort.least_issues=Fæst vandamál

@ -1512,6 +1512,7 @@ milestones.modify=Aggiorna pietra miliare
milestones.deletion=Elimina pietra miliare
milestones.deletion_desc=Eliminare una pietra miliare la rimuove da tutte le relative issue. Continuare?
milestones.deletion_success=La pietra miliare è stata eliminata.
milestones.filter_sort.name=Nome
milestones.filter_sort.least_complete=Meno completato
milestones.filter_sort.most_complete=Più completato
milestones.filter_sort.most_issues=Più problemi

@ -1933,6 +1933,7 @@ milestones.edit_success=マイルストーン "%s" を更新しました。
milestones.deletion=マイルストーンの削除
milestones.deletion_desc=マイルストーンを削除すると、関連するすべてのイシューから除去されます。 続行しますか?
milestones.deletion_success=マイルストーンを削除しました。
milestones.filter_sort.name=名称
milestones.filter_sort.earliest_due_data=期日が早い順
milestones.filter_sort.latest_due_date=期日が遅い順
milestones.filter_sort.least_complete=消化率の低い順

@ -875,6 +875,7 @@ milestones.modify=마일스톤 갱신
milestones.deletion=마일스톤 삭제
milestones.deletion_desc=마일스톤을 삭제하면 연관된 모든 이슈에서 삭제됩니다. 계속 하시겠습니까?
milestones.deletion_success=마일스톤이 삭제되었습니다.
milestones.filter_sort.name=이름
milestones.filter_sort.least_complete=완료율이 낮은 순
milestones.filter_sort.most_complete=완료율이 높은 순
milestones.filter_sort.most_issues=이슈 많은 순

@ -1828,6 +1828,7 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab
milestones.deletion=Dzēst atskaites punktu
milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts.
milestones.filter_sort.name=Nosaukums
milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks
milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks
milestones.filter_sort.least_complete=Vismazāk pabeigtais

@ -1507,6 +1507,7 @@ milestones.modify=Mijlpaal bijwerken
milestones.deletion=Mijlpaal verwijderen
milestones.deletion_desc=Als je een mijlpaal verwijdert, wordt hij van alle gerelateerde kwesties verwijderd. Doorgaan?
milestones.deletion_success=De mijlpaal is verwijderd.
milestones.filter_sort.name=Naam
milestones.filter_sort.least_complete=Minst compleet
milestones.filter_sort.most_complete=Meest compleet
milestones.filter_sort.most_issues=Meeste problemen

@ -1360,6 +1360,7 @@ milestones.modify=Zaktualizuj cel
milestones.deletion=Usuń kamień milowy
milestones.deletion_desc=Usunięcie celu usuwa go z wszystkich pozostałych zagadnień. Kontynuować?
milestones.deletion_success=Cel został usunięty.
milestones.filter_sort.name=Nazwa
milestones.filter_sort.least_complete=Najmniej kompletne
milestones.filter_sort.most_complete=Najbardziej kompletne
milestones.filter_sort.most_issues=Najwięcej zgłoszeń

@ -1820,6 +1820,7 @@ milestones.edit_success=O marco "%s" foi atualizado.
milestones.deletion=Excluir marco
milestones.deletion_desc=A exclusão deste marco irá removê-lo de todas as issues. Tem certeza que deseja continuar?
milestones.deletion_success=O marco foi excluído.
milestones.filter_sort.name=Nome
milestones.filter_sort.earliest_due_data=Data limite mais próxima
milestones.filter_sort.latest_due_date=Data limite mais distante
milestones.filter_sort.least_complete=Menos completo

@ -1933,6 +1933,7 @@ milestones.edit_success=A etapa "%s" foi modificada.
milestones.deletion=Eliminar etapa
milestones.deletion_desc=Se eliminar uma etapa, irá removê-la de todas as questões relacionadas. Quer continuar?
milestones.deletion_success=A etapa foi eliminada.
milestones.filter_sort.name=Nome
milestones.filter_sort.earliest_due_data=Data de vencimento mais próxima
milestones.filter_sort.latest_due_date=Data de vencimento mais distante
milestones.filter_sort.least_complete=Menos completo

@ -1789,6 +1789,7 @@ milestones.edit_success=Этап «%s» обновлён.
milestones.deletion=Удалить этап
milestones.deletion_desc=Удаление этапа приведет к его удалению из всех связанных задач. Продолжить?
milestones.deletion_success=Этап успешно удалён.
milestones.filter_sort.name=Название
milestones.filter_sort.earliest_due_data=По возрастанию даты завершения
milestones.filter_sort.latest_due_date=По убыванию даты завершения
milestones.filter_sort.least_complete=Менее полное

@ -1352,6 +1352,7 @@ milestones.modify=සන්ධිස්ථානයක් යාවත්කා
milestones.deletion=සන්ධිස්ථානය මකන්න
milestones.deletion_desc=සන්ධිස්ථානයක් මකා දැමීම සම්බන්ධ සියලු ගැටළු වලින් එය ඉවත් කරයි. දිගටම?
milestones.deletion_success=සන්ධිස්ථානය මකා දමා ඇත.
milestones.filter_sort.name=නම
milestones.filter_sort.least_complete=අවම වශයෙන් සම්පූර්ණයි
milestones.filter_sort.most_complete=වඩාත්ම සම්පූර්ණයි
milestones.filter_sort.most_issues=බොහෝ ප්රශ්න

@ -1132,6 +1132,7 @@ milestones.modify=Uppdatera milstolpe
milestones.deletion=Ta bort milstolpe
milestones.deletion_desc=Borttagning av en milstolpe tar bort den från samtliga relaterade ärende. Fortsätta?
milestones.deletion_success=Milstolpen har blivit borttagen.
milestones.filter_sort.name=Namn
milestones.filter_sort.least_complete=Minst klar
milestones.filter_sort.most_complete=Mest klar
milestones.filter_sort.most_issues=Mest ärenden

@ -1920,6 +1920,7 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
milestones.deletion=Kilometre Taşını Sil
milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
milestones.deletion_success=Kilometre taşı silindi.
milestones.filter_sort.name=İsim
milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
milestones.filter_sort.least_complete=En az tamamlama

@ -1399,6 +1399,7 @@ milestones.modify=Оновити етап
milestones.deletion=Видалити етап
milestones.deletion_desc=Видалення етапу призведе до його видалення з усіх пов'язаних задач. Продовжити?
milestones.deletion_success=Етап успішно видалено.
milestones.filter_sort.name=Назва
milestones.filter_sort.least_complete=Менш повне
milestones.filter_sort.most_complete=Більш повне
milestones.filter_sort.most_issues=Найбільш задач

@ -1923,6 +1923,7 @@ milestones.edit_success=里程碑 %s 已经更新。
milestones.deletion=删除里程碑
milestones.deletion_desc=删除该里程碑将会移除所有工单中相关的信息。是否继续?
milestones.deletion_success=里程碑已被删除。
milestones.filter_sort.name=名称
milestones.filter_sort.earliest_due_data=到期日从远到近
milestones.filter_sort.latest_due_date=到期日从近到远
milestones.filter_sort.least_complete=完成度从低到高

@ -511,6 +511,7 @@ milestones.due_date=截止日期(可選)
milestones.clear=清除
milestones.edit=編輯里程碑
milestones.cancel=取消
milestones.filter_sort.name=組織名稱
milestones.filter_sort.least_complete=完成度由低到高
milestones.filter_sort.most_complete=完成度由高到低
milestones.filter_sort.most_issues=問題由多到少

@ -1656,6 +1656,7 @@ milestones.edit_success=已更新里程碑「%s」。
milestones.deletion=刪除里程碑
milestones.deletion_desc=刪除里程碑會從所有相關的問題移除它。是否繼續?
milestones.deletion_success=里程碑已刪除
milestones.filter_sort.name=名稱
milestones.filter_sort.least_complete=完成度由低到高
milestones.filter_sort.most_complete=完成度由高到低
milestones.filter_sort.most_issues=問題由多到少

@ -622,10 +622,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
// update external user information
if gothUser != nil {
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) {
log.Error("UpdateExternalUser failed: %v", err)
}
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
log.Error("EnsureLinkExternalToUser failed: %v", err)
}
}

@ -27,7 +27,6 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
@ -1148,9 +1147,39 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
groups := getClaimedGroups(oauth2Source, &gothUser)
opts := &user_service.UpdateOptions{}
// Reactivate user if they are deactivated
if !u.IsActive {
opts.IsActive = optional.Some(true)
}
// Update GroupClaims
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
ctx.ServerError("EnsureLinkExternalToUser", err)
return
}
// If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA {
// Register last login
opts.SetLastLogin = true
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := updateSession(ctx, nil, map[string]any{
"uid": u.ID,
"uname": u.Name,
@ -1162,29 +1191,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
opts := &user_service.UpdateOptions{
SetLastLogin: true,
}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
// update external user information
if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
if !errors.Is(err, util.ErrNotExist) {
log.Error("UpdateExternalUser failed: %v", err)
}
}
if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err)
return
@ -1200,22 +1206,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
opts := &user_service.UpdateOptions{}
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
}
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := updateSession(ctx, nil, map[string]any{
// User needs to use 2FA, save data and redirect to 2FA page.
"twofaUid": u.ID,

@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"testing"
"code.gitea.io/gitea/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{})
}

@ -0,0 +1,62 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
type fakeProvider struct{}
func (p *fakeProvider) Name() string {
return "fake"
}
func (p *fakeProvider) SetName(name string) {}
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) {
return nil, nil
}
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) {
return nil, nil
}
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) {
return goth.User{}, nil
}
func (p *fakeProvider) Debug(bool) {
}
func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
switch refreshToken {
case "expired":
return nil, &oauth2.RetrieveError{
ErrorCode: "invalid_grant",
}
default:
return &oauth2.Token{
AccessToken: "token",
TokenType: "Bearer",
RefreshToken: "refresh",
Expiry: time.Now().Add(time.Hour),
}, nil
}
}
func (p *fakeProvider) RefreshTokenAvailable() bool {
return true
}
func init() {
RegisterGothProvider(
NewSimpleProvider("fake", "Fake", []string{"account"},
func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider {
return &fakeProvider{}
}))
}

@ -36,7 +36,7 @@ func (source *Source) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &source)
}
// ToDB exports an SMTPConfig to a serialized format.
// ToDB exports an OAuth2Config to a serialized format.
func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}

@ -0,0 +1,114 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"context"
"time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
// Sync causes this OAuth2 source to synchronize its users with the db.
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID)
if !updateExisting {
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name)
return nil
}
provider, err := createProvider(source.authSource.Name, source)
if err != nil {
return err
}
if !provider.RefreshTokenAvailable() {
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name)
return nil
}
opts := user_model.FindExternalUserOptions{
HasRefreshToken: true,
Expired: true,
LoginSourceID: source.authSource.ID,
}
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
return source.refresh(ctx, provider, u)
})
}
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error {
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt)
shouldDisable := false
token, err := provider.RefreshToken(u.RefreshToken)
if err != nil {
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" {
// this signals that the token is not valid and the user should be disabled
shouldDisable = true
} else {
return err
}
}
user := &user_model.User{
LoginName: u.ExternalID,
LoginType: auth.OAuth2,
LoginSource: u.LoginSourceID,
}
hasUser, err := user_model.GetUser(ctx, user)
if err != nil {
return err
}
// If the grant is no longer valid, disable the user and
// delete local tokens. If the OAuth2 provider still
// recognizes them as a valid user, they will be able to login
// via their provider and reactivate their account.
if shouldDisable {
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID)
return db.WithTx(ctx, func(ctx context.Context) error {
if hasUser {
user.IsActive = false
err := user_model.UpdateUserCols(ctx, user, "is_active")
if err != nil {
return err
}
}
// Delete stored tokens, since they are invalid. This
// also provents us from checking this in subsequent runs.
u.AccessToken = ""
u.RefreshToken = ""
u.ExpiresAt = time.Time{}
return user_model.UpdateExternalUserByExternalID(ctx, u)
})
}
// Otherwise, update the tokens
u.AccessToken = token.AccessToken
u.ExpiresAt = token.Expiry
// Some providers only update access tokens provide a new
// refresh token, so avoid updating it if it's empty
if token.RefreshToken != "" {
u.RefreshToken = token.RefreshToken
}
err = user_model.UpdateExternalUserByExternalID(ctx, u)
return err
}

@ -0,0 +1,100 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"context"
"testing"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestSource(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
source := &Source{
Provider: "fake",
authSource: &auth.Source{
ID: 12,
Type: auth.OAuth2,
Name: "fake",
IsActive: true,
IsSyncEnabled: true,
},
}
user := &user_model.User{
LoginName: "external",
LoginType: auth.OAuth2,
LoginSource: source.authSource.ID,
Name: "test",
Email: "external@example.com",
}
err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{})
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: "external",
UserID: user.ID,
LoginSourceID: user.LoginSource,
RefreshToken: "valid",
}
err = user_model.LinkExternalToUser(context.Background(), user, e)
assert.NoError(t, err)
provider, err := createProvider(source.authSource.Name, source)
assert.NoError(t, err)
t.Run("refresh", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
err := source.refresh(context.Background(), provider, e)
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: e.ExternalID,
LoginSourceID: e.LoginSourceID,
}
ok, err := user_model.GetExternalLogin(context.Background(), e)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, e.RefreshToken, "refresh")
assert.Equal(t, e.AccessToken, "token")
u, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.True(t, u.IsActive)
})
t.Run("expired", func(t *testing.T) {
err := source.refresh(context.Background(), provider, &user_model.ExternalLoginUser{
ExternalID: "external",
UserID: user.ID,
LoginSourceID: user.LoginSource,
RefreshToken: "expired",
})
assert.NoError(t, err)
e := &user_model.ExternalLoginUser{
ExternalID: e.ExternalID,
LoginSourceID: e.LoginSourceID,
}
ok, err := user_model.GetExternalLogin(context.Background(), e)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, e.RefreshToken, "")
assert.Equal(t, e.AccessToken, "")
u, err := user_model.GetUserByID(context.Background(), user.ID)
assert.NoError(t, err)
assert.False(t, u.IsActive)
})
})
}

@ -71,14 +71,14 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
return nil
}
// UpdateExternalUser updates external user's information
func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
// EnsureLinkExternalToUser link the gothUser to the user
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
if err != nil {
return err
}
return user_model.UpdateExternalUserByExternalID(ctx, externalLoginUser)
return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
}
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID

@ -314,7 +314,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
for _, recipient := range recipients {
msg := NewMessageFrom(
recipient.Email,
ctx.Doer.GetCompleteName(),
fromDisplayName(ctx.Doer),
setting.MailService.FromEmail,
subject,
mailBody.String(),
@ -536,3 +536,19 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
}
return typeName, name, template
}
func fromDisplayName(u *user_model.User) string {
if setting.MailService.FromDisplayNameFormatTemplate != nil {
var ctx bytes.Buffer
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
"DisplayName": u.DisplayName(),
"AppName": setting.AppName,
"Domain": setting.Domain,
})
if err == nil {
return mime.QEncoding.Encode("utf-8", ctx.String())
}
log.Error("fromDisplayName: %w", err)
}
return u.GetCompleteName()
}

@ -86,7 +86,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
}
msgs := make([]*Message, 0, len(tos))
publisherName := rel.Publisher.DisplayName()
publisherName := fromDisplayName(rel.Publisher)
msgID := generateMessageIDForRelease(rel)
for _, to := range tos {
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())

@ -79,7 +79,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
}
for _, to := range emailTos {
msg := NewMessage(to.EmailTo(), subject, content.String())
msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID)
SendAsync(msg)

@ -403,3 +403,51 @@ func TestGenerateMessageIDForRelease(t *testing.T) {
})
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
}
func TestFromDisplayName(t *testing.T) {
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
assert.NoError(t, err)
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
defer func() { setting.MailService = nil }()
tests := []struct {
userDisplayName string
fromDisplayName string
}{{
userDisplayName: "test",
fromDisplayName: "test",
}, {
userDisplayName: "Hi Its <Mee>",
fromDisplayName: "Hi Its <Mee>",
}, {
userDisplayName: "Æsir",
fromDisplayName: "=?utf-8?q?=C3=86sir?=",
}, {
userDisplayName: "new😀user",
fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
}}
for _, tc := range tests {
t.Run(tc.userDisplayName, func(t *testing.T) {
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
got := fromDisplayName(user)
assert.EqualValues(t, tc.fromDisplayName, got)
})
}
t.Run("template with all available vars", func(t *testing.T) {
template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
assert.NoError(t, err)
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
oldAppName := setting.AppName
setting.AppName = "Code IT"
oldDomain := setting.Domain
setting.Domain = "code.it"
defer func() {
setting.AppName = oldAppName
setting.Domain = oldDomain
}()
assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
})
}

@ -412,7 +412,7 @@
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
</div>
{{end}}
{{if .Source.IsLDAP}}
{{if (or .Source.IsLDAP .Source.IsOAuth2)}}
<div class="inline field">
<div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>

@ -59,7 +59,7 @@
<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
</div>
</div>
<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
<div class="oauth2 ldap inline field {{if not (or (eq .type 2) (eq .type 6))}}tw-hidden{{end}}">
<div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>

@ -36,7 +36,7 @@
</div>
</div>
<div class="ui bottom attached segment tw-p-0">
<div class="ui active tab tw-rounded" data-tab="write">
<div class="ui active tab tw-rounded-b" data-tab="write">
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
data-url="{{.Repository.Link}}/markup"
data-context="{{.RepoLink}}"

@ -11,5 +11,6 @@
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
</div>
</div>

@ -65,6 +65,7 @@
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
</div>
</div>
</div>

@ -224,7 +224,8 @@ func TestLDAPUserSync(t *testing.T) {
}
defer tests.PrepareTestEnv(t)()
addAuthSourceLDAP(t, "", "")
auth.SyncExternalUsers(context.Background(), true)
err := auth.SyncExternalUsers(context.Background(), true)
assert.NoError(t, err)
// Check if users exists
for _, gitLDAPUser := range gitLDAPUsers {

@ -23,18 +23,7 @@
.monaco-editor,
.monaco-editor .overflow-guard {
border-radius: var(--border-radius);
}
/* these seem unthemeable */
.monaco-scrollable-element > .scrollbar > .slider {
background: var(--color-primary) !important;
}
.monaco-scrollable-element > .scrollbar > .slider:hover {
background: var(--color-primary-dark-1) !important;
}
.monaco-scrollable-element > .scrollbar > .slider:active {
background: var(--color-primary-dark-2) !important;
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* fomantic styles destroy this element only visible on IOS, restore it */

@ -212,6 +212,7 @@
--color-button: #171a1e;
--color-code-bg: #14171a;
--color-shadow: #00001758;
--color-shadow-opaque: #000017;
--color-secondary-bg: #2a3137;
--color-expand-button: #2f363d;
--color-placeholder-text: var(--color-text-light-3);

@ -212,6 +212,7 @@
--color-button: #f8f9fb;
--color-code-bg: #fafdff;
--color-shadow: #00001726;
--color-shadow-opaque: #c7ced5;
--color-secondary-bg: #f2f5f8;
--color-expand-button: #cfe8fa;
--color-placeholder-text: var(--color-text-light-3);

@ -96,7 +96,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
'input.background': getColor('--color-input-background'),
'input.border': getColor('--color-input-border'),
'input.foreground': getColor('--color-input-text'),
'scrollbar.shadow': getColor('--color-shadow'),
'scrollbar.shadow': getColor('--color-shadow-opaque'),
'progressBar.background': getColor('--color-primary'),
'focusBorder': '#0000', // prevent blue border
},

@ -125,7 +125,7 @@ export function initRepoCommentForm() {
$listMenu.find('.item:not(.no-select)').on('click', function (e) {
e.preventDefault();
if ($(this).hasClass('ban-change')) {
if (this.classList.contains('ban-change')) {
return false;
}
@ -140,7 +140,7 @@ export function initRepoCommentForm() {
if (this.getAttribute('data-scope') !== scope) {
return true;
}
if (this !== clickedItem && !$(this).hasClass('checked')) {
if (this !== clickedItem && !this.classList.contains('checked')) {
return true;
}
} else if (this !== clickedItem) {
@ -148,7 +148,7 @@ export function initRepoCommentForm() {
return true;
}
if ($(this).hasClass('checked')) {
if (this.classList.contains('checked')) {
$(this).removeClass('checked');
$(this).find('.octicon-check').addClass('tw-invisible');
if (hasUpdateAction) {
@ -187,7 +187,7 @@ export function initRepoCommentForm() {
const listIds = [];
$(this).parent().find('.item').each(function () {
if ($(this).hasClass('checked')) {
if (this.classList.contains('checked')) {
listIds.push($(this).data('id'));
$($(this).data('id-selector')).removeClass('tw-hidden');
} else {