Merge branch 'master' into feat/approval-new

This commit is contained in:
Jonas Franz 2018-05-17 14:00:21 +02:00
commit 0f64dad74b
No known key found for this signature in database
GPG Key ID: 506AEEBE80BEDECD
57 changed files with 2090 additions and 988 deletions

View File

@ -25,6 +25,7 @@ var (
subcmdCreateUser, subcmdCreateUser,
subcmdChangePassword, subcmdChangePassword,
subcmdRepoSyncReleases, subcmdRepoSyncReleases,
subcmdRegenerate,
}, },
} }
@ -80,6 +81,41 @@ var (
Usage: "Synchronize repository releases with tags", Usage: "Synchronize repository releases with tags",
Action: runRepoSyncReleases, Action: runRepoSyncReleases,
} }
subcmdRegenerate = cli.Command{
Name: "regenerate",
Usage: "Regenerate specific files",
Subcommands: []cli.Command{
microcmdRegenHooks,
microcmdRegenKeys,
},
}
microcmdRegenHooks = cli.Command{
Name: "hooks",
Usage: "Regenerate git-hooks",
Action: runRegenerateHooks,
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
},
}
microcmdRegenKeys = cli.Command{
Name: "keys",
Usage: "Regenerate authorized_keys file",
Action: runRegenerateKeys,
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
},
}
) )
func runChangePassword(c *cli.Context) error { func runChangePassword(c *cli.Context) error {
@ -195,3 +231,25 @@ func getReleaseCount(id int64) (int64, error) {
}, },
) )
} }
func runRegenerateHooks(c *cli.Context) error {
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
if err := initDB(); err != nil {
return err
}
return models.SyncRepositoryHooks()
}
func runRegenerateKeys(c *cli.Context) error {
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
if err := initDB(); err != nil {
return err
}
return models.RewriteAllPublicKeys()
}

View File

@ -2,5 +2,5 @@
[[ -f ./setup ]] && source ./setup [[ -f ./setup ]] && source ./setup
pushd /app/gitea > /dev/null pushd /app/gitea > /dev/null
exec su-exec git /app/gitea/gitea web exec su-exec $USER /app/gitea/gitea web
popd popd

View File

@ -39,5 +39,5 @@ if [ ! -f /data/gitea/conf/app.ini ]; then
envsubst < /etc/templates/app.ini > /data/gitea/conf/app.ini envsubst < /etc/templates/app.ini > /data/gitea/conf/app.ini
fi fi
chown -R git:git /data/gitea /app/gitea /data/git chown -R ${USER}:git /data/gitea /app/gitea /data/git
chmod 0755 /data/gitea /app/gitea /data/git chmod 0755 /data/gitea /app/gitea /data/git

View File

@ -1,5 +1,12 @@
#!/bin/sh #!/bin/sh
if [ "${USER}" != "git" ]; then
# rename user
sed -i -e "s/^git\:/${USER}\:/g" /etc/passwd
# switch sshd config to different user
sed -i -e "s/AllowUsers git/AllowUsers ${USER}/g" /etc/ssh/sshd_config
fi
## Change GID for USER? ## Change GID for USER?
if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ]; then if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ]; then
sed -i -e "s/^${USER}:\([^:]*\):[0-9]*/${USER}:\1:${USER_GID}/" /etc/group sed -i -e "s/^${USER}:\([^:]*\):[0-9]*/${USER}:\1:${USER_GID}/" /etc/group

View File

@ -537,7 +537,7 @@ _Symbols used in table:_
</tr> </tr>
<tr> <tr>
<td>Webhook support</td> <td>Webhook support</td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>

View File

@ -64,6 +64,13 @@ Admin operations:
- `--password value`, `-p value`: New password. Required. - `--password value`, `-p value`: New password. Required.
- Examples: - Examples:
- `gitea admin change-password --username myname --password asecurepassword` - `gitea admin change-password --username myname --password asecurepassword`
- `regenerate`
- Options:
- `hooks`: Regenerate git-hooks for all repositories
- `keys`: Regenerate authorized_keys file
- Examples:
- `gitea admin regenerate hooks`
- `gitea admin regenerate keys`
#### cert #### cert

View File

@ -618,6 +618,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
case ActionDeleteBranch: // Delete Branch case ActionDeleteBranch: // Delete Branch
isHookEventPush = true isHookEventPush = true
if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
Ref: refName,
RefType: "branch",
PusherType: api.PusherTypeUser,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
}
case ActionPushTag: // Create case ActionPushTag: // Create
isHookEventPush = true isHookEventPush = true
@ -640,6 +650,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
} }
case ActionDeleteTag: // Delete Tag case ActionDeleteTag: // Delete Tag
isHookEventPush = true isHookEventPush = true
if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
Ref: refName,
RefType: "tag",
PusherType: api.PusherTypeUser,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
}
} }
if isHookEventPush { if isHookEventPush {

View File

@ -89,9 +89,10 @@ const (
type Comment struct { type Comment struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
Type CommentType Type CommentType
PosterID int64 `xorm:"INDEX"` PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"` Poster *User `xorm:"-"`
IssueID int64 `xorm:"INDEX"` IssueID int64 `xorm:"INDEX"`
Issue *Issue `xorm:"-"`
LabelID int64 LabelID int64
Label *Label `xorm:"-"` Label *Label `xorm:"-"`
OldMilestoneID int64 OldMilestoneID int64
@ -127,6 +128,15 @@ type Comment struct {
Invalidated bool Invalidated bool
} }
// LoadIssue loads issue from database
func (c *Comment) LoadIssue() (err error) {
if c.Issue != nil {
return nil
}
c.Issue, err = GetIssueByID(c.IssueID)
return
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
func (c *Comment) AfterLoad(session *xorm.Session) { func (c *Comment) AfterLoad(session *xorm.Session) {
var err error var err error
@ -157,40 +167,40 @@ func (c *Comment) AfterDelete() {
// HTMLURL formats a URL-string to the issue-comment // HTMLURL formats a URL-string to the issue-comment
func (c *Comment) HTMLURL() string { func (c *Comment) HTMLURL() string {
issue, err := GetIssueByID(c.IssueID) err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused: if err != nil { // Silently dropping errors :unamused:
log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return "" return ""
} }
return fmt.Sprintf("%s#%s", issue.HTMLURL(), c.HashTag()) return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
} }
// IssueURL formats a URL-string to the issue // IssueURL formats a URL-string to the issue
func (c *Comment) IssueURL() string { func (c *Comment) IssueURL() string {
issue, err := GetIssueByID(c.IssueID) err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused: if err != nil { // Silently dropping errors :unamused:
log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return "" return ""
} }
if issue.IsPull { if c.Issue.IsPull {
return "" return ""
} }
return issue.HTMLURL() return c.Issue.HTMLURL()
} }
// PRURL formats a URL-string to the pull-request // PRURL formats a URL-string to the pull-request
func (c *Comment) PRURL() string { func (c *Comment) PRURL() string {
issue, err := GetIssueByID(c.IssueID) err := c.LoadIssue()
if err != nil { // Silently dropping errors :unamused: if err != nil { // Silently dropping errors :unamused:
log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err) log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
return "" return ""
} }
if !issue.IsPull { if !c.Issue.IsPull {
return "" return ""
} }
return issue.HTMLURL() return c.Issue.HTMLURL()
} }
// APIFormat converts a Comment to the api.Comment format // APIFormat converts a Comment to the api.Comment format
@ -207,9 +217,14 @@ func (c *Comment) APIFormat() *api.Comment {
} }
} }
// CommentHashTag returns unique hash tag for comment id.
func CommentHashTag(id int64) string {
return fmt.Sprintf("issuecomment-%d", id)
}
// HashTag returns unique hash tag for comment. // HashTag returns unique hash tag for comment.
func (c *Comment) HashTag() string { func (c *Comment) HashTag() string {
return "issuecomment-" + com.ToStr(c.ID) return CommentHashTag(c.ID)
} }
// EventTag returns unique event hash tag for comment. // EventTag returns unique event hash tag for comment.
@ -638,7 +653,7 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
// CreateIssueComment creates a plain issue comment. // CreateIssueComment creates a plain issue comment.
func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
return CreateComment(&CreateCommentOptions{ comment, err := CreateComment(&CreateCommentOptions{
Type: CommentTypeComment, Type: CommentTypeComment,
Doer: doer, Doer: doer,
Repo: repo, Repo: repo,
@ -646,6 +661,21 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
Content: content, Content: content,
Attachments: attachments, Attachments: attachments,
}) })
if err != nil {
return nil, fmt.Errorf("CreateComment: %v", err)
}
mode, _ := AccessLevel(doer.ID, repo)
if err = PrepareWebhooks(repo, HookEventIssueComment, &api.IssueCommentPayload{
Action: api.HookIssueCommentCreated,
Issue: issue.APIFormat(),
Comment: comment.APIFormat(),
Repository: repo.APIFormat(mode),
Sender: doer.APIFormat(),
}); err != nil {
log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
}
return comment, nil
} }
// CreateCodeComment creates a plain code comment at the specified line / path // CreateCodeComment creates a plain code comment at the specified line / path
@ -798,17 +828,41 @@ func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
} }
// UpdateComment updates information of comment. // UpdateComment updates information of comment.
func UpdateComment(c *Comment) error { func UpdateComment(doer *User, c *Comment, oldContent string) error {
if _, err := x.ID(c.ID).AllCols().Update(c); err != nil { if _, err := x.ID(c.ID).AllCols().Update(c); err != nil {
return err return err
} else if c.Type == CommentTypeComment { } else if c.Type == CommentTypeComment {
UpdateIssueIndexer(c.IssueID) UpdateIssueIndexer(c.IssueID)
} }
if err := c.LoadIssue(); err != nil {
return err
}
if err := c.Issue.LoadAttributes(); err != nil {
return err
}
mode, _ := AccessLevel(doer.ID, c.Issue.Repo)
if err := PrepareWebhooks(c.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
Action: api.HookIssueCommentEdited,
Issue: c.Issue.APIFormat(),
Comment: c.APIFormat(),
Changes: &api.ChangesPayload{
Body: &api.ChangesFromPayload{
From: oldContent,
},
},
Repository: c.Issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
}); err != nil {
log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
}
return nil return nil
} }
// DeleteComment deletes the comment // DeleteComment deletes the comment
func DeleteComment(comment *Comment) error { func DeleteComment(doer *User, comment *Comment) error {
sess := x.NewSession() sess := x.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
@ -835,6 +889,26 @@ func DeleteComment(comment *Comment) error {
} else if comment.Type == CommentTypeComment { } else if comment.Type == CommentTypeComment {
UpdateIssueIndexer(comment.IssueID) UpdateIssueIndexer(comment.IssueID)
} }
if err := comment.LoadIssue(); err != nil {
return err
}
if err := comment.Issue.LoadAttributes(); err != nil {
return err
}
mode, _ := AccessLevel(doer.ID, comment.Issue.Repo)
if err := PrepareWebhooks(comment.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
Action: api.HookIssueCommentDeleted,
Issue: comment.Issue.APIFormat(),
Comment: comment.APIFormat(),
Repository: comment.Issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
}); err != nil {
log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
}
return nil return nil
} }

View File

@ -5,6 +5,9 @@
package models package models
import ( import (
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
@ -358,7 +361,49 @@ func ChangeMilestoneAssign(issue *Issue, doer *User, oldMilestoneID int64) (err
if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil { if err = changeMilestoneAssign(sess, doer, issue, oldMilestoneID); err != nil {
return err return err
} }
return sess.Commit()
if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
}
var hookAction api.HookIssueAction
if issue.MilestoneID > 0 {
hookAction = api.HookIssueMilestoned
} else {
hookAction = api.HookIssueDemilestoned
}
if err = issue.LoadAttributes(); err != nil {
return err
}
mode, _ := AccessLevel(doer.ID, issue.Repo)
if issue.IsPull {
err = issue.PullRequest.LoadIssue()
if err != nil {
log.Error(2, "LoadIssue: %v", err)
return
}
err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{
Action: hookAction,
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
} else {
err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{
Action: hookAction,
Index: issue.Index,
Issue: issue.APIFormat(),
Repository: issue.Repo.APIFormat(mode),
Sender: doer.APIFormat(),
})
}
if err != nil {
log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
}
return nil
} }
// DeleteMilestoneByRepoID deletes a milestone from a repository. // DeleteMilestoneByRepoID deletes a milestone from a repository.

View File

@ -232,6 +232,8 @@ func TestChangeMilestoneAssign(t *testing.T) {
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue) issue := AssertExistsAndLoadBean(t, &Issue{RepoID: 1}).(*Issue)
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
assert.NotNil(t, issue)
assert.NotNil(t, doer)
oldMilestoneID := issue.MilestoneID oldMilestoneID := issue.MilestoneID
issue.MilestoneID = 2 issue.MilestoneID = 2

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/git" "code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -190,8 +191,27 @@ func CreateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []stri
} }
err = addReleaseAttachments(rel.ID, attachmentUUIDs) err = addReleaseAttachments(rel.ID, attachmentUUIDs)
if err != nil {
return err
}
return err if !rel.IsDraft {
if err := rel.LoadAttributes(); err != nil {
log.Error(2, "LoadAttributes: %v", err)
} else {
mode, _ := AccessLevel(rel.PublisherID, rel.Repo)
if err := PrepareWebhooks(rel.Repo, HookEventRelease, &api.ReleasePayload{
Action: api.HookReleasePublished,
Release: rel.APIFormat(),
Repository: rel.Repo.APIFormat(mode),
Sender: rel.Publisher.APIFormat(),
}); err != nil {
log.Error(2, "PrepareWebhooks: %v", err)
}
}
}
return nil
} }
// GetRelease returns release by given ID. // GetRelease returns release by given ID.

View File

@ -2456,6 +2456,17 @@ func ForkRepository(doer, u *User, oldRepo *Repository, name, desc string) (_ *R
return nil, err return nil, err
} }
oldMode, _ := AccessLevel(doer.ID, oldRepo)
mode, _ := AccessLevel(doer.ID, repo)
if err = PrepareWebhooks(oldRepo, HookEventFork, &api.ForkPayload{
Forkee: repo.APIFormat(mode),
Repo: oldRepo.APIFormat(oldMode),
Sender: doer.APIFormat(),
}); err != nil {
log.Error(2, "PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err)
}
if err = repo.UpdateSize(); err != nil { if err = repo.UpdateSize(); err != nil {
log.Error(4, "Failed to update size for repository: %v", err) log.Error(4, "Failed to update size for repository: %v", err)
} }

View File

@ -66,10 +66,15 @@ func IsValidHookContentType(name string) bool {
// HookEvents is a set of web hook events // HookEvents is a set of web hook events
type HookEvents struct { type HookEvents struct {
Create bool `json:"create"` Create bool `json:"create"`
Push bool `json:"push"` Delete bool `json:"delete"`
PullRequest bool `json:"pull_request"` Fork bool `json:"fork"`
Repository bool `json:"repository"` Issues bool `json:"issues"`
IssueComment bool `json:"issue_comment"`
Push bool `json:"push"`
PullRequest bool `json:"pull_request"`
Repository bool `json:"repository"`
Release bool `json:"release"`
} }
// HookEvent represents events that will delivery hook. // HookEvent represents events that will delivery hook.
@ -155,6 +160,30 @@ func (w *Webhook) HasCreateEvent() bool {
(w.ChooseEvents && w.HookEvents.Create) (w.ChooseEvents && w.HookEvents.Create)
} }
// HasDeleteEvent returns true if hook enabled delete event.
func (w *Webhook) HasDeleteEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Delete)
}
// HasForkEvent returns true if hook enabled fork event.
func (w *Webhook) HasForkEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Fork)
}
// HasIssuesEvent returns true if hook enabled issues event.
func (w *Webhook) HasIssuesEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Issues)
}
// HasIssueCommentEvent returns true if hook enabled issue_comment event.
func (w *Webhook) HasIssueCommentEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.IssueComment)
}
// HasPushEvent returns true if hook enabled push event. // HasPushEvent returns true if hook enabled push event.
func (w *Webhook) HasPushEvent() bool { func (w *Webhook) HasPushEvent() bool {
return w.PushOnly || w.SendEverything || return w.PushOnly || w.SendEverything ||
@ -167,23 +196,46 @@ func (w *Webhook) HasPullRequestEvent() bool {
(w.ChooseEvents && w.HookEvents.PullRequest) (w.ChooseEvents && w.HookEvents.PullRequest)
} }
// HasReleaseEvent returns if hook enabled release event.
func (w *Webhook) HasReleaseEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Release)
}
// HasRepositoryEvent returns if hook enabled repository event. // HasRepositoryEvent returns if hook enabled repository event.
func (w *Webhook) HasRepositoryEvent() bool { func (w *Webhook) HasRepositoryEvent() bool {
return w.SendEverything || return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Repository) (w.ChooseEvents && w.HookEvents.Repository)
} }
func (w *Webhook) eventCheckers() []struct {
has func() bool
typ HookEventType
} {
return []struct {
has func() bool
typ HookEventType
}{
{w.HasCreateEvent, HookEventCreate},
{w.HasDeleteEvent, HookEventDelete},
{w.HasForkEvent, HookEventFork},
{w.HasPushEvent, HookEventPush},
{w.HasIssuesEvent, HookEventIssues},
{w.HasIssueCommentEvent, HookEventIssueComment},
{w.HasPullRequestEvent, HookEventPullRequest},
{w.HasRepositoryEvent, HookEventRepository},
{w.HasReleaseEvent, HookEventRelease},
}
}
// EventsArray returns an array of hook events // EventsArray returns an array of hook events
func (w *Webhook) EventsArray() []string { func (w *Webhook) EventsArray() []string {
events := make([]string, 0, 3) events := make([]string, 0, 7)
if w.HasCreateEvent() {
events = append(events, "create") for _, c := range w.eventCheckers() {
} if c.has() {
if w.HasPushEvent() { events = append(events, string(c.typ))
events = append(events, "push") }
}
if w.HasPullRequestEvent() {
events = append(events, "pull_request")
} }
return events return events
} }
@ -373,10 +425,15 @@ type HookEventType string
// Types of hook events // Types of hook events
const ( const (
HookEventCreate HookEventType = "create" HookEventCreate HookEventType = "create"
HookEventPush HookEventType = "push" HookEventDelete HookEventType = "delete"
HookEventPullRequest HookEventType = "pull_request" HookEventFork HookEventType = "fork"
HookEventRepository HookEventType = "repository" HookEventPush HookEventType = "push"
HookEventIssues HookEventType = "issues"
HookEventIssueComment HookEventType = "issue_comment"
HookEventPullRequest HookEventType = "pull_request"
HookEventRepository HookEventType = "repository"
HookEventRelease HookEventType = "release"
) )
// HookRequest represents hook task request information. // HookRequest represents hook task request information.
@ -488,22 +545,11 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
} }
func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error { func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
switch event { for _, e := range w.eventCheckers() {
case HookEventCreate: if event == e.typ {
if !w.HasCreateEvent() { if !e.has() {
return nil return nil
} }
case HookEventPush:
if !w.HasPushEvent() {
return nil
}
case HookEventPullRequest:
if !w.HasPullRequestEvent() {
return nil
}
case HookEventRepository:
if !w.HasRepositoryEvent() {
return nil
} }
} }

View File

@ -49,6 +49,38 @@ func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
}, nil }, nil
} }
func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) {
// created tag/branch
refName := git.RefEndName(p.Ref)
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
return &DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: title,
Title: title,
HideAvatar: "0",
SingleTitle: fmt.Sprintf("view branch %s", refName),
SingleURL: p.Repo.HTMLURL + "/src/" + refName,
},
}, nil
}
func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return &DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: title,
Title: title,
HideAvatar: "0",
SingleTitle: fmt.Sprintf("view forked repo %s", p.Repo.FullName),
SingleURL: p.Repo.HTMLURL,
},
}, nil
}
func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
var ( var (
branchName = git.RefEndName(p.Ref) branchName = git.RefEndName(p.Ref)
@ -98,6 +130,80 @@ func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
}, nil }, nil
} }
func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) {
var text, title string
switch p.Action {
case api.HookIssueOpened:
title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueClosed:
title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueReOpened:
title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueEdited:
title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueAssigned:
title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueLabelUpdated:
title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueLabelCleared:
title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
case api.HookIssueSynchronized:
title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
}
return &DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: text,
Title: title,
HideAvatar: "0",
SingleTitle: "view pull request",
SingleURL: p.Issue.URL,
},
}, nil
}
func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) {
title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
var content string
switch p.Action {
case api.HookIssueCommentCreated:
title = "New comment: " + title
content = p.Comment.Body
case api.HookIssueCommentEdited:
title = "Comment edited: " + title
content = p.Comment.Body
case api.HookIssueCommentDeleted:
title = "Comment deleted: " + title
url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
content = p.Comment.Body
}
return &DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: content,
Title: title,
HideAvatar: "0",
SingleTitle: "view pull request",
SingleURL: url,
},
}, nil
}
func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) {
var text, title string var text, title string
switch p.Action { switch p.Action {
@ -182,6 +288,27 @@ func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, e
return nil, nil return nil, nil
} }
func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) {
var title, url string
switch p.Action {
case api.HookReleasePublished:
title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
url = p.Release.URL
return &DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: title,
Title: title,
HideAvatar: "0",
SingleTitle: "view repository",
SingleURL: url,
},
}, nil
}
return nil, nil
}
// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload // GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) { func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*DingtalkPayload, error) {
s := new(DingtalkPayload) s := new(DingtalkPayload)
@ -189,12 +316,22 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType, meta string) (*Din
switch event { switch event {
case HookEventCreate: case HookEventCreate:
return getDingtalkCreatePayload(p.(*api.CreatePayload)) return getDingtalkCreatePayload(p.(*api.CreatePayload))
case HookEventDelete:
return getDingtalkDeletePayload(p.(*api.DeletePayload))
case HookEventFork:
return getDingtalkForkPayload(p.(*api.ForkPayload))
case HookEventIssues:
return getDingtalkIssuesPayload(p.(*api.IssuePayload))
case HookEventIssueComment:
return getDingtalkIssueCommentPayload(p.(*api.IssueCommentPayload))
case HookEventPush: case HookEventPush:
return getDingtalkPushPayload(p.(*api.PushPayload)) return getDingtalkPushPayload(p.(*api.PushPayload))
case HookEventPullRequest: case HookEventPullRequest:
return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
case HookEventRepository: case HookEventRepository:
return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload))
case HookEventRelease:
return getDingtalkReleasePayload(p.(*api.ReleasePayload))
} }
return s, nil return s, nil

View File

@ -115,6 +115,51 @@ func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordP
}, nil }, nil
} }
func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) {
// deleted tag/branch
refName := git.RefEndName(p.Ref)
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
return &DiscordPayload{
Username: meta.Username,
AvatarURL: meta.IconURL,
Embeds: []DiscordEmbed{
{
Title: title,
URL: p.Repo.HTMLURL + "/src/" + refName,
Color: warnColor,
Author: DiscordEmbedAuthor{
Name: p.Sender.UserName,
URL: setting.AppURL + p.Sender.UserName,
IconURL: p.Sender.AvatarURL,
},
},
},
}, nil
}
func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) {
// fork
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return &DiscordPayload{
Username: meta.Username,
AvatarURL: meta.IconURL,
Embeds: []DiscordEmbed{
{
Title: title,
URL: p.Repo.HTMLURL,
Color: successColor,
Author: DiscordEmbedAuthor{
Name: p.Sender.UserName,
URL: setting.AppURL + p.Sender.UserName,
IconURL: p.Sender.AvatarURL,
},
},
},
}, nil
}
func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) { func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
var ( var (
branchName = git.RefEndName(p.Ref) branchName = git.RefEndName(p.Ref)
@ -165,6 +210,108 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo
}, nil }, nil
} }
func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) {
var text, title string
var color int
switch p.Action {
case api.HookIssueOpened:
title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueClosed:
title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
color = failedColor
text = p.Issue.Body
case api.HookIssueReOpened:
title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueEdited:
title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueAssigned:
title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = successColor
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueLabelUpdated:
title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueLabelCleared:
title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
case api.HookIssueSynchronized:
title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
text = p.Issue.Body
color = warnColor
}
return &DiscordPayload{
Username: meta.Username,
AvatarURL: meta.IconURL,
Embeds: []DiscordEmbed{
{
Title: title,
Description: text,
URL: p.Issue.URL,
Color: color,
Author: DiscordEmbedAuthor{
Name: p.Sender.UserName,
URL: setting.AppURL + p.Sender.UserName,
IconURL: p.Sender.AvatarURL,
},
},
},
}, nil
}
func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) {
title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
content := ""
var color int
switch p.Action {
case api.HookIssueCommentCreated:
title = "New comment: " + title
content = p.Comment.Body
color = successColor
case api.HookIssueCommentEdited:
title = "Comment edited: " + title
content = p.Comment.Body
color = warnColor
case api.HookIssueCommentDeleted:
title = "Comment deleted: " + title
url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
content = p.Comment.Body
color = warnColor
}
return &DiscordPayload{
Username: discord.Username,
AvatarURL: discord.IconURL,
Embeds: []DiscordEmbed{
{
Title: title,
Description: content,
URL: url,
Color: color,
Author: DiscordEmbedAuthor{
Name: p.Sender.UserName,
URL: setting.AppURL + p.Sender.UserName,
IconURL: p.Sender.AvatarURL,
},
},
},
}, nil
}
func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) { func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
var text, title string var text, title string
var color int var color int
@ -267,6 +414,35 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*
}, nil }, nil
} }
func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) {
var title, url string
var color int
switch p.Action {
case api.HookReleasePublished:
title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
url = p.Release.URL
color = successColor
}
return &DiscordPayload{
Username: meta.Username,
AvatarURL: meta.IconURL,
Embeds: []DiscordEmbed{
{
Title: title,
Description: fmt.Sprintf("%s", p.Release.Note),
URL: url,
Color: color,
Author: DiscordEmbedAuthor{
Name: p.Sender.UserName,
URL: setting.AppURL + p.Sender.UserName,
IconURL: p.Sender.AvatarURL,
},
},
},
}, nil
}
// GetDiscordPayload converts a discord webhook into a DiscordPayload // GetDiscordPayload converts a discord webhook into a DiscordPayload
func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) { func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
s := new(DiscordPayload) s := new(DiscordPayload)
@ -279,12 +455,22 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc
switch event { switch event {
case HookEventCreate: case HookEventCreate:
return getDiscordCreatePayload(p.(*api.CreatePayload), discord) return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
case HookEventDelete:
return getDiscordDeletePayload(p.(*api.DeletePayload), discord)
case HookEventFork:
return getDiscordForkPayload(p.(*api.ForkPayload), discord)
case HookEventIssues:
return getDiscordIssuesPayload(p.(*api.IssuePayload), discord)
case HookEventIssueComment:
return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord)
case HookEventPush: case HookEventPush:
return getDiscordPushPayload(p.(*api.PushPayload), discord) return getDiscordPushPayload(p.(*api.PushPayload), discord)
case HookEventPullRequest: case HookEventPullRequest:
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
case HookEventRepository: case HookEventRepository:
return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
case HookEventRelease:
return getDiscordReleasePayload(p.(*api.ReleasePayload), discord)
} }
return s, nil return s, nil

View File

@ -106,6 +106,122 @@ func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayloa
}, nil }, nil
} }
// getSlackDeletePayload composes Slack payload for delete a branch or tag.
func getSlackDeletePayload(p *api.DeletePayload, slack *SlackMeta) (*SlackPayload, error) {
refName := git.RefEndName(p.Ref)
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
return &SlackPayload{
Channel: slack.Channel,
Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
}, nil
}
// getSlackForkPayload composes Slack payload for forked by a repository.
func getSlackForkPayload(p *api.ForkPayload, slack *SlackMeta) (*SlackPayload, error) {
baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
return &SlackPayload{
Channel: slack.Channel,
Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
}, nil
}
func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
fmt.Sprintf("#%d %s", p.Index, p.Issue.Title))
var text, title, attachmentText string
switch p.Action {
case api.HookIssueOpened:
text = fmt.Sprintf("[%s] Issue submitted by %s", p.Repository.FullName, senderLink)
title = titleLink
attachmentText = SlackTextFormatter(p.Issue.Body)
case api.HookIssueClosed:
text = fmt.Sprintf("[%s] Issue closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
case api.HookIssueReOpened:
text = fmt.Sprintf("[%s] Issue re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
case api.HookIssueEdited:
text = fmt.Sprintf("[%s] Issue edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
attachmentText = SlackTextFormatter(p.Issue.Body)
case api.HookIssueAssigned:
text = fmt.Sprintf("[%s] Issue assigned to %s: %s by %s", p.Repository.FullName,
SlackLinkFormatter(setting.AppURL+p.Issue.Assignee.UserName, p.Issue.Assignee.UserName),
titleLink, senderLink)
case api.HookIssueUnassigned:
text = fmt.Sprintf("[%s] Issue unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
case api.HookIssueLabelUpdated:
text = fmt.Sprintf("[%s] Issue labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
case api.HookIssueLabelCleared:
text = fmt.Sprintf("[%s] Issue labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
case api.HookIssueSynchronized:
text = fmt.Sprintf("[%s] Issue synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink)
}
return &SlackPayload{
Channel: slack.Channel,
Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
Attachments: []SlackAttachment{{
Color: slack.Color,
Title: title,
Text: attachmentText,
}},
}, nil
}
func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)),
fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
var text, title, attachmentText string
switch p.Action {
case api.HookIssueCommentCreated:
text = fmt.Sprintf("[%s] New comment created by %s", p.Repository.FullName, senderLink)
title = titleLink
attachmentText = SlackTextFormatter(p.Comment.Body)
case api.HookIssueCommentEdited:
text = fmt.Sprintf("[%s] Comment edited by %s", p.Repository.FullName, senderLink)
title = titleLink
attachmentText = SlackTextFormatter(p.Comment.Body)
case api.HookIssueCommentDeleted:
text = fmt.Sprintf("[%s] Comment deleted by %s", p.Repository.FullName, senderLink)
title = SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index),
fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
attachmentText = SlackTextFormatter(p.Comment.Body)
}
return &SlackPayload{
Channel: slack.Channel,
Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
Attachments: []SlackAttachment{{
Color: slack.Color,
Title: title,
Text: attachmentText,
}},
}, nil
}
func getSlackReleasePayload(p *api.ReleasePayload, slack *SlackMeta) (*SlackPayload, error) {
repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
refLink := SlackLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
text := fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName)
return &SlackPayload{
Channel: slack.Channel,
Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
}, nil
}
func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) { func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
// n new commits // n new commits
var ( var (
@ -238,12 +354,22 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP
switch event { switch event {
case HookEventCreate: case HookEventCreate:
return getSlackCreatePayload(p.(*api.CreatePayload), slack) return getSlackCreatePayload(p.(*api.CreatePayload), slack)
case HookEventDelete:
return getSlackDeletePayload(p.(*api.DeletePayload), slack)
case HookEventFork:
return getSlackForkPayload(p.(*api.ForkPayload), slack)
case HookEventIssues:
return getSlackIssuesPayload(p.(*api.IssuePayload), slack)
case HookEventIssueComment:
return getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
case HookEventPush: case HookEventPush:
return getSlackPushPayload(p.(*api.PushPayload), slack) return getSlackPushPayload(p.(*api.PushPayload), slack)
case HookEventPullRequest: case HookEventPullRequest:
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
case HookEventRepository: case HookEventRepository:
return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
case HookEventRelease:
return getSlackReleasePayload(p.(*api.ReleasePayload), slack)
} }
return s, nil return s, nil

View File

@ -73,7 +73,7 @@ func TestWebhook_UpdateEvent(t *testing.T) {
} }
func TestWebhook_EventsArray(t *testing.T) { func TestWebhook_EventsArray(t *testing.T) {
assert.Equal(t, []string{"create", "push", "pull_request"}, assert.Equal(t, []string{"create", "delete", "fork", "push", "issues", "issue_comment", "pull_request", "repository", "release"},
(&Webhook{ (&Webhook{
HookEvent: &HookEvent{SendEverything: true}, HookEvent: &HookEvent{SendEverything: true},
}).EventsArray(), }).EventsArray(),

View File

@ -155,12 +155,17 @@ func (f *ProtectBranchForm) Validate(ctx *macaron.Context, errs binding.Errors)
// WebhookForm form for changing web hook // WebhookForm form for changing web hook
type WebhookForm struct { type WebhookForm struct {
Events string Events string
Create bool Create bool
Push bool Delete bool
PullRequest bool Fork bool
Repository bool Issues bool
Active bool IssueComment bool
Release bool
Push bool
PullRequest bool
Repository bool
Active bool
} }
// PushOnly if the hook will be triggered when push // PushOnly if the hook will be triggered when push

View File

@ -184,7 +184,7 @@ func (f *AddKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding
// NewAccessTokenForm form for creating access token // NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct { type NewAccessTokenForm struct {
Name string `binding:"Required"` Name string `binding:"Required;MaxSize(255)"`
} }
// Validate valideates the fields // Validate valideates the fields

View File

@ -311,7 +311,6 @@ security=Sicherheit
avatar=Profilbild avatar=Profilbild
ssh_gpg_keys=SSH / GPG Schlüssel ssh_gpg_keys=SSH / GPG Schlüssel
social=Soziale Konten social=Soziale Konten
applications=Zugriffstoken
orgs=Organisationen verwalten orgs=Organisationen verwalten
repos=Repositories repos=Repositories
delete=Konto löschen delete=Konto löschen

View File

@ -1011,6 +1011,16 @@ settings.event_send_everything = All Events
settings.event_choose = Custom Events… settings.event_choose = Custom Events…
settings.event_create = Create settings.event_create = Create
settings.event_create_desc = Branch or tag created. settings.event_create_desc = Branch or tag created.
settings.event_delete = Delete
settings.event_delete_desc = Branch or tag deleted
settings.event_fork = Fork
settings.event_fork_desc = Repository forked
settings.event_issues = Issues
settings.event_issues_desc = Issue opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, milestoned, or demilestoned.
settings.event_issue_comment = Issue Comment
settings.event_issue_comment_desc = Issue comment created, edited, or deleted.
settings.event_release = Release
settings.event_release_desc = Release published in a repository.
settings.event_pull_request = Pull Request settings.event_pull_request = Pull Request
settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared or synchronized. settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared or synchronized.
settings.event_push = Push settings.event_push = Push

View File

@ -306,12 +306,13 @@ form.name_pattern_not_allowed=O padrão de '%s' não é permitido em um nome de
[settings] [settings]
profile=Perfil profile=Perfil
account=Conta
password=Senha password=Senha
security=Segurança security=Segurança
avatar=Avatar avatar=Avatar
ssh_gpg_keys=Chaves SSH / GPG ssh_gpg_keys=Chaves SSH / GPG
social=Contas sociais social=Contas sociais
applications=Tokens de acesso applications=Aplicações
orgs=Gerenciar organizações orgs=Gerenciar organizações
repos=Repositórios repos=Repositórios
delete=Excluir conta delete=Excluir conta
@ -334,7 +335,7 @@ continue=Continuar
cancel=Cancelar cancel=Cancelar
language=Idioma language=Idioma
lookup_avatar_by_mail=Procure o avatar do endereço de e-mail lookup_avatar_by_mail=Procurar o avatar do endereço de e-mail
federated_avatar_lookup=Busca de avatar federativo federated_avatar_lookup=Busca de avatar federativo
enable_custom_avatar=Habilitar avatar customizado enable_custom_avatar=Habilitar avatar customizado
choose_new_avatar=Escolha um novo avatar choose_new_avatar=Escolha um novo avatar
@ -344,7 +345,7 @@ uploaded_avatar_not_a_image=O arquivo enviado não é uma imagem.
update_avatar_success=Seu avatar foi atualizado. update_avatar_success=Seu avatar foi atualizado.
change_password=Atualizar senha change_password=Atualizar senha
old_password=Senha Atual old_password=Senha atual
new_password=Nova senha new_password=Nova senha
retype_new_password=Digite a nova senha novamente retype_new_password=Digite a nova senha novamente
password_incorrect=A senha atual está incorreta. password_incorrect=A senha atual está incorreta.

View File

@ -155,14 +155,14 @@ org_no_results=Відповідних організацій не знайден
code_search_results=Результати пошуку '%s' code_search_results=Результати пошуку '%s'
[auth] [auth]
create_new_account=Реєстрація аккаунта create_new_account=Реєстрація облікового запису
register_helper_msg=Вже зареєстровані? Увійдіть зараз! register_helper_msg=Вже зареєстровані? Увійдіть зараз!
disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту. disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту.
disable_register_mail=Підтвердження реєстрації електронною поштою вимкнено. disable_register_mail=Підтвердження реєстрації електронною поштою вимкнено.
remember_me=Запам'ятати мене remember_me=Запам'ятати мене
forgot_password_title=Забув пароль forgot_password_title=Забув пароль
forgot_password=Забули пароль? forgot_password=Забули пароль?
sign_up_now=Потрібен аккаунт? Зареєструватися. sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз.
confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації. confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
reset_password_mail_sent_prompt=Лист для підтвердження було відправлено на <b>%s</b>. Будь ласка, перевірте вашу поштову скриньку протягом %s для скидання пароля. reset_password_mail_sent_prompt=Лист для підтвердження було відправлено на <b>%s</b>. Будь ласка, перевірте вашу поштову скриньку протягом %s для скидання пароля.
active_your_account=Активувати обліковий запис active_your_account=Активувати обліковий запис
@ -264,16 +264,18 @@ form.name_reserved=Ім'я користувача "%s" зарезервован
[settings] [settings]
profile=Профіль profile=Профіль
account=Обліковий запис
password=Пароль password=Пароль
security=Безпека security=Безпека
avatar=Аватар avatar=Аватар
ssh_gpg_keys=SSH / GPG ключі ssh_gpg_keys=SSH / GPG ключі
social=Соціальні акаунти social=Соціальні облікові записи
applications=Токени Доступу applications=Додатки
orgs=Керування організаціями orgs=Керування організаціями
repos=Репозиторії repos=Репозиторії
delete=Видалити обліковий запис delete=Видалити обліковий запис
twofa=Двофакторна авторизація twofa=Двофакторна авторизація
account_link=Прив'язані облікові записи
organization=Організації organization=Організації
uid=Ідентифікатор Uid uid=Ідентифікатор Uid
@ -339,7 +341,7 @@ show_openid=Показати у профілю
hide_openid=Не показувати у профілі hide_openid=Не показувати у профілі
ssh_disabled=SSH вимкнено ssh_disabled=SSH вимкнено
manage_social=Керувати зв'язаними аккаунтами соціальних мереж manage_social=Керувати зв'язаними обліковими записами соціальних мереж
unbind=Від'єднати unbind=Від'єднати
generate_new_token=Згенерувати новий токен generate_new_token=Згенерувати новий токен
@ -350,6 +352,8 @@ delete_token=Видалити
twofa_disable=Вимкнути двофакторну автентифікацію twofa_disable=Вимкнути двофакторну автентифікацію
or_enter_secret=Або введіть секрет: %s or_enter_secret=Або введіть секрет: %s
manage_account_links=Керування обліковими записами
remove_account_link=Видалити облікові записи
delete_account=Видалити ваш обліковий запис delete_account=Видалити ваш обліковий запис
@ -479,6 +483,7 @@ issues.new.open_milestone=Активні етапи
issues.new.closed_milestone=Закриті етапи issues.new.closed_milestone=Закриті етапи
issues.new.assignees=Виконавеці issues.new.assignees=Виконавеці
issues.new.clear_assignees=Прибрати виконавеців issues.new.clear_assignees=Прибрати виконавеців
issues.new.no_assignees=Ніхто не призначений
issues.no_ref=Не вказана гілка або тег issues.no_ref=Не вказана гілка або тег
issues.create=Створити проблему issues.create=Створити проблему
issues.new_label=Нова мітка issues.new_label=Нова мітка
@ -592,6 +597,7 @@ pulls.tab_commits=Коміти
pulls.tab_files=Змінені файли pulls.tab_files=Змінені файли
pulls.reopen_to_merge=Будь ласка перевідкрийте цей запит щоб здіснити операцію злиття. pulls.reopen_to_merge=Будь ласка перевідкрийте цей запит щоб здіснити операцію злиття.
pulls.merged=Злито pulls.merged=Злито
pulls.has_merged=Запит на злиття було об'єднано.
pulls.can_auto_merge_desc=Цей запит можна об'єднати автоматично. pulls.can_auto_merge_desc=Цей запит можна об'єднати автоматично.
pulls.merge_pull_request=Об'єднати запит на злиття pulls.merge_pull_request=Об'єднати запит на злиття
@ -671,6 +677,7 @@ search.search_repo=Пошук репозиторію
settings=Налаштування settings=Налаштування
settings.options=Репозиторій settings.options=Репозиторій
settings.collaboration=Співробітники
settings.collaboration.admin=Адміністратор settings.collaboration.admin=Адміністратор
settings.collaboration.write=Запис settings.collaboration.write=Запис
settings.collaboration.read=Читати settings.collaboration.read=Читати
@ -696,6 +703,7 @@ settings.admin_settings=Налаштування адміністратора
settings.danger_zone=Небезпечна зона settings.danger_zone=Небезпечна зона
settings.new_owner_has_same_repo=Новий власник вже має репозиторій з такою назвою. Будь ласка, виберіть інше ім'я. settings.new_owner_has_same_repo=Новий власник вже має репозиторій з такою назвою. Будь ласка, виберіть інше ім'я.
settings.convert=Перетворити на звичайний репозиторій settings.convert=Перетворити на звичайний репозиторій
settings.convert_desc=Ви можете сконвертувати це дзеркало у звичайний репозиторій. Це не може бути скасовано.
settings.transfer=Передати новому власнику settings.transfer=Передати новому власнику
settings.wiki_delete=Видалити Wiki-дані settings.wiki_delete=Видалити Wiki-дані
settings.confirm_wiki_delete=Видалити Wiki-дані settings.confirm_wiki_delete=Видалити Wiki-дані
@ -724,6 +732,7 @@ settings.slack_icon_url=URL іконки
settings.discord_username=Ім'я кристувача settings.discord_username=Ім'я кристувача
settings.discord_icon_url=URL іконки settings.discord_icon_url=URL іконки
settings.slack_color=Колір settings.slack_color=Колір
settings.event_push_only=Push події
settings.event_send_everything=Всі події settings.event_send_everything=Всі події
settings.event_create=Створити settings.event_create=Створити
settings.event_create_desc=Гілку або тег створено. settings.event_create_desc=Гілку або тег створено.
@ -811,6 +820,7 @@ org_desc=Опис
team_name=Назва команди team_name=Назва команди
team_desc=Опис team_desc=Опис
team_permission_desc=Права доступу team_permission_desc=Права доступу
team_unit_desc=Дозволити доступ до розділів репозиторію
settings=Налаштування settings=Налаштування
@ -862,11 +872,13 @@ last_page=Остання
total=Разом: %d total=Разом: %d
dashboard.statistic=Підсумок dashboard.statistic=Підсумок
dashboard.system_status=Статус системи
dashboard.operation_name=Назва операції dashboard.operation_name=Назва операції
dashboard.operation_switch=Перемкнути dashboard.operation_switch=Перемкнути
dashboard.operation_run=Запустити dashboard.operation_run=Запустити
dashboard.delete_inactivate_accounts=Видалити всі неактивні облікові записи dashboard.delete_inactivate_accounts=Видалити всі неактивні облікові записи
dashboard.delete_inactivate_accounts_success=Усі неактивні облікові записи успішно видалено. dashboard.delete_inactivate_accounts_success=Усі неактивні облікові записи успішно видалено.
dashboard.git_gc_repos_success=Всі репозиторії завершили збирання сміття.
dashboard.server_uptime=Uptime серверу dashboard.server_uptime=Uptime серверу
dashboard.current_memory_usage=Поточне використання пам'яті dashboard.current_memory_usage=Поточне використання пам'яті
dashboard.total_memory_allocated=Виділено пам'яті загалом dashboard.total_memory_allocated=Виділено пам'яті загалом
@ -877,9 +889,14 @@ dashboard.mspan_structures_obtained=Отримано структур MSpan
dashboard.mcache_structures_usage=Використання структур MCache dashboard.mcache_structures_usage=Використання структур MCache
dashboard.mcache_structures_obtained=Отримано структур MCache dashboard.mcache_structures_obtained=Отримано структур MCache
dashboard.profiling_bucket_hash_table_obtained=Отримано хеш-таблиць профілювання dashboard.profiling_bucket_hash_table_obtained=Отримано хеш-таблиць профілювання
dashboard.gc_metadata_obtained=Отримано метаданих GC dashboard.gc_metadata_obtained=Отримано метаданих збирача сміття (GC)
dashboard.other_system_allocation_obtained=Отримання інших виділень пам'яті dashboard.other_system_allocation_obtained=Отримання інших виділень пам'яті
dashboard.next_gc_recycle=Наступний цикл GC dashboard.next_gc_recycle=Наступний цикл збирача сміття (GC)
dashboard.last_gc_time=З останнього запуску збирача сміття (GC)
dashboard.total_gc_time=Загальна пауза збирача сміття (GC)
dashboard.total_gc_pause=Загальна пауза збирача сміття (GC)
dashboard.last_gc_pause=Остання пауза збирача сміття (GC)
dashboard.gc_times=Кількість запусків збирача сміття (GC)
users.user_manage_panel=Керування обліковими записами користувачів users.user_manage_panel=Керування обліковими записами користувачів
users.new_account=Створити обліковий запис users.new_account=Створити обліковий запис
@ -893,6 +910,7 @@ users.send_register_notify=Надіслати повідомлення про р
users.edit=Редагувати users.edit=Редагувати
users.auth_source=Джерело автентифікації users.auth_source=Джерело автентифікації
users.local=Локальні users.local=Локальні
users.edit_account=Редагувати обліковий запис
users.max_repo_creation=Максимальна кількість репозиторіїв users.max_repo_creation=Максимальна кількість репозиторіїв
users.max_repo_creation_desc=(Введіть -1, щоб використовувати глобальний ліміт за замовчуванням.) users.max_repo_creation_desc=(Введіть -1, щоб використовувати глобальний ліміт за замовчуванням.)
users.is_activated=Обліковий запис користувача увімкнено users.is_activated=Обліковий запис користувача увімкнено
@ -1036,7 +1054,7 @@ config.session_provider=Провайдер сесії
config.provider_config=Конфігурація постачальника config.provider_config=Конфігурація постачальника
config.cookie_name=Ім'я файлу cookie config.cookie_name=Ім'я файлу cookie
config.enable_set_cookie=Увімкнути встановлення cookie config.enable_set_cookie=Увімкнути встановлення cookie
config.gc_interval_time=Інтервал запуску GC config.gc_interval_time=Інтервал запуску збирача сміття (GC)
config.session_life_time=Час життя сесії config.session_life_time=Час життя сесії
config.https_only=Тільки HTTPS config.https_only=Тільки HTTPS
config.cookie_life_time=Час життя cookie-файлу config.cookie_life_time=Час життя cookie-файлу
@ -1050,12 +1068,12 @@ config.git_disable_diff_highlight=Вимкнути підсвітку синта
config.git_max_diff_lines=Максимум рядків на diff (на один файл) config.git_max_diff_lines=Максимум рядків на diff (на один файл)
config.git_max_diff_line_characters=Максимум символів на diff (на одну строку) config.git_max_diff_line_characters=Максимум символів на diff (на одну строку)
config.git_max_diff_files=Максимум diff-файлів (для показу) config.git_max_diff_files=Максимум diff-файлів (для показу)
config.git_gc_args=Аргументи GC config.git_gc_args=Аргументи збирача сміття (GC)
config.git_migrate_timeout=Тайм-аут міграції config.git_migrate_timeout=Тайм-аут міграції
config.git_mirror_timeout=Тайм-аут оновлення дзеркала config.git_mirror_timeout=Тайм-аут оновлення дзеркала
config.git_clone_timeout=Тайм-аут операції клонування config.git_clone_timeout=Тайм-аут операції клонування
config.git_pull_timeout=Тайм-аут операції Pull config.git_pull_timeout=Тайм-аут операції Pull
config.git_gc_timeout=Тайм-аут операції GC config.git_gc_timeout=Тайм-аут операції збирача сміття (GC)
config.log_config=Конфігурація журналу config.log_config=Конфігурація журналу
config.log_mode=Режим журналювання config.log_mode=Режим журналювання

View File

@ -306,12 +306,13 @@ form.name_pattern_not_allowed=用户名中不允许使用 "%s"。
[settings] [settings]
profile=个人信息 profile=个人信息
account=账号
password=修改密码 password=修改密码
security=安全 security=安全
avatar=头像设置 avatar=头像设置
ssh_gpg_keys=SSH / GPG 密钥 ssh_gpg_keys=SSH / GPG 密钥
social=社交帐号绑定 social=社交帐号绑定
applications=访问令牌(Access Tokens) applications=应用
orgs=管理组织 orgs=管理组织
repos=仓库列表 repos=仓库列表
delete=删除帐户 delete=删除帐户
@ -329,7 +330,7 @@ location=所在地区
update_profile=更新信息 update_profile=更新信息
update_profile_success=您的资料信息已经更新 update_profile_success=您的资料信息已经更新
change_username=您的用户名已更改。 change_username=您的用户名已更改。
change_username_prompt=注意:更改账户名将同时改变账户的URL change_username_prompt=注意:更改账号名将同时改变账号的URL
continue=继续操作 continue=继续操作
cancel=取消操作 cancel=取消操作
language=界面语言 language=界面语言
@ -379,7 +380,7 @@ manage_ssh_keys=管理 SSH 密钥
manage_gpg_keys=管理 GPG 密钥 manage_gpg_keys=管理 GPG 密钥
add_key=增加密钥 add_key=增加密钥
ssh_desc=这些 SSH 公钥已经关联到你的账号。相应的私钥拥有完全操作你的仓库的权限。 ssh_desc=这些 SSH 公钥已经关联到你的账号。相应的私钥拥有完全操作你的仓库的权限。
gpg_desc=这些 GPG 公钥已经关联到你的账。请妥善保管你的私钥因为他们将被用于认证提交。 gpg_desc=这些 GPG 公钥已经关联到你的账。请妥善保管你的私钥因为他们将被用于认证提交。
ssh_helper=<strong>需要帮助?</strong> 请查看有关 <a href="%s">如何生成 SSH 密钥</a> 或 <a href="%s">常见 SSH 问题</a> 寻找答案。 ssh_helper=<strong>需要帮助?</strong> 请查看有关 <a href="%s">如何生成 SSH 密钥</a> 或 <a href="%s">常见 SSH 问题</a> 寻找答案。
gpg_helper=<strong>需要帮助吗?</strong>看一看 GitHub <a href="%s">关于GPG</a> 的指导。 gpg_helper=<strong>需要帮助吗?</strong>看一看 GitHub <a href="%s">关于GPG</a> 的指导。
add_new_key=增加 SSH 密钥 add_new_key=增加 SSH 密钥
@ -422,31 +423,31 @@ unbind_success=社会帐户已从您的帐户中解除绑定。
manage_access_token=管理Access Tokens manage_access_token=管理Access Tokens
generate_new_token=生成新的令牌 generate_new_token=生成新的令牌
tokens_desc=这些令牌拥有通过 Gitea API 对您的帐户的访问权限。 tokens_desc=这些令牌拥有通过 Gitea API 对您的帐户的访问权限。
new_token_desc=使用令牌的应用拥有完全访问你的账的权限。 new_token_desc=使用令牌的应用拥有完全访问你的账的权限。
token_name=令牌名称 token_name=令牌名称
generate_token=生成令牌 generate_token=生成令牌
generate_token_success=新令牌生成成功。请拷贝因为令牌将只会显示一次。 generate_token_success=新令牌生成成功。请拷贝因为令牌将只会显示一次。
delete_token=删除令牌 delete_token=删除令牌
access_token_deletion=删除Access Tokens access_token_deletion=删除Access Tokens
access_token_deletion_desc=删除一个令牌将会组织通过它访问你账号的应用。是否继续? access_token_deletion_desc=删除一个令牌将会组织通过它访问你账号的应用。是否继续?
delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账 delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账
twofa_desc=两步验证可以加强你的账安全性。 twofa_desc=两步验证可以加强你的账安全性。
twofa_is_enrolled=你的账<strong>已启用</strong>了两步验证。 twofa_is_enrolled=你的账<strong>已启用</strong>了两步验证。
twofa_not_enrolled=你的账号未开启两步验证。 twofa_not_enrolled=你的账号未开启两步验证。
twofa_disable=禁用两步认证 twofa_disable=禁用两步认证
twofa_scratch_token_regenerate=重新生成初始令牌 twofa_scratch_token_regenerate=重新生成初始令牌
twofa_scratch_token_regenerated=你的初始令牌是 %s。请将它保存到一个安全的地方。 twofa_scratch_token_regenerated=你的初始令牌是 %s。请将它保存到一个安全的地方。
twofa_enroll=启用两步验证 twofa_enroll=启用两步验证
twofa_disable_note=如果需要, 可以禁用双因素身份验证。 twofa_disable_note=如果需要, 可以禁用双因素身份验证。
twofa_disable_desc=关掉两步验证会使得您的账不安全,继续执行? twofa_disable_desc=关掉两步验证会使得您的账不安全,继续执行?
regenerate_scratch_token_desc=如果您丢失了您的验证口令或已经使用它登录, 您可以在这里重置它。 regenerate_scratch_token_desc=如果您丢失了您的验证口令或已经使用它登录, 您可以在这里重置它。
twofa_disabled=两步验证已被禁用。 twofa_disabled=两步验证已被禁用。
scan_this_image=使用您的授权应用扫描这张图片: scan_this_image=使用您的授权应用扫描这张图片:
or_enter_secret=或者输入密钥:%s or_enter_secret=或者输入密钥:%s
then_enter_passcode=并输入应用程序中显示的密码: then_enter_passcode=并输入应用程序中显示的密码:
passcode_invalid=密码不正确。再试一次。 passcode_invalid=密码不正确。再试一次。
twofa_enrolled=你的账已经启用了两步验证。请保存初始令牌(%s到一个安全的地方此令牌仅当前显示一次。 twofa_enrolled=你的账已经启用了两步验证。请保存初始令牌(%s到一个安全的地方此令牌仅当前显示一次。
manage_account_links=管理绑定过的账号 manage_account_links=管理绑定过的账号
manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。 manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。
@ -999,6 +1000,16 @@ settings.event_send_everything=所有事件
settings.event_choose=自定义事件... settings.event_choose=自定义事件...
settings.event_create=创建 settings.event_create=创建
settings.event_create_desc=创建分支或标签 settings.event_create_desc=创建分支或标签
settings.event_delete=刪除
settings.event_delete_desc=删除分支或标签
settings.event_fork=派生
settings.event_fork_desc=仓库被派生
settings.event_issues=工单
settings.event_issues_desc=工单被开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签、设置里程碑或取消设置里程碑
settings.event_issue_comment=工单评论
settings.event_issue_comment_desc=工单评论被创建、编辑或删除
settings.event_release=版本发布
settings.event_release_desc=仓库发布新的版本。
settings.event_pull_request=合并请求 settings.event_pull_request=合并请求
settings.event_pull_request_desc=开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签或同步合并请求 settings.event_pull_request_desc=开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签或同步合并请求
settings.event_push=推送 settings.event_push=推送
@ -1129,7 +1140,7 @@ branch.restore_failed=未能还原分支%s。
branch.protected_deletion_failed=分支 '%s' 已被保护,不可删除。 branch.protected_deletion_failed=分支 '%s' 已被保护,不可删除。
topic.manage_topics=管理主题 topic.manage_topics=管理主题
topic.done=已完成 topic.done=保存
[org] [org]
org_name_holder=组织名称 org_name_holder=组织名称

View File

@ -261,8 +261,9 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
oldContent := comment.Content
comment.Content = form.Body comment.Content = form.Body
if err := models.UpdateComment(comment); err != nil { if err := models.UpdateComment(ctx.User, comment, oldContent); err != nil {
ctx.Error(500, "UpdateComment", err) ctx.Error(500, "UpdateComment", err)
return return
} }
@ -348,7 +349,7 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if err = models.DeleteComment(comment); err != nil { if err = models.DeleteComment(ctx.User, comment); err != nil {
ctx.Error(500, "DeleteCommentByID", err) ctx.Error(500, "DeleteCommentByID", err)
return return
} }

View File

@ -7,12 +7,13 @@ package utils
import ( import (
api "code.gitea.io/sdk/gitea" api "code.gitea.io/sdk/gitea"
"encoding/json"
"net/http"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/convert"
"encoding/json"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"net/http"
) )
// GetOrgHook get an organization's webhook. If there is an error, write to // GetOrgHook get an organization's webhook. If there is an error, write to
@ -98,9 +99,15 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
HookEvent: &models.HookEvent{ HookEvent: &models.HookEvent{
ChooseEvents: true, ChooseEvents: true,
HookEvents: models.HookEvents{ HookEvents: models.HookEvents{
Create: com.IsSliceContainsStr(form.Events, string(models.HookEventCreate)), Create: com.IsSliceContainsStr(form.Events, string(models.HookEventCreate)),
Push: com.IsSliceContainsStr(form.Events, string(models.HookEventPush)), Delete: com.IsSliceContainsStr(form.Events, string(models.HookEventDelete)),
PullRequest: com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest)), Fork: com.IsSliceContainsStr(form.Events, string(models.HookEventFork)),
Issues: com.IsSliceContainsStr(form.Events, string(models.HookEventIssues)),
IssueComment: com.IsSliceContainsStr(form.Events, string(models.HookEventIssueComment)),
Push: com.IsSliceContainsStr(form.Events, string(models.HookEventPush)),
PullRequest: com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest)),
Repository: com.IsSliceContainsStr(form.Events, string(models.HookEventRepository)),
Release: com.IsSliceContainsStr(form.Events, string(models.HookEventRelease)),
}, },
}, },
IsActive: form.Active, IsActive: form.Active,
@ -211,6 +218,16 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho
w.Create = com.IsSliceContainsStr(form.Events, string(models.HookEventCreate)) w.Create = com.IsSliceContainsStr(form.Events, string(models.HookEventCreate))
w.Push = com.IsSliceContainsStr(form.Events, string(models.HookEventPush)) w.Push = com.IsSliceContainsStr(form.Events, string(models.HookEventPush))
w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest)) w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
w.Create = com.IsSliceContainsStr(form.Events, string(models.HookEventCreate))
w.Delete = com.IsSliceContainsStr(form.Events, string(models.HookEventDelete))
w.Fork = com.IsSliceContainsStr(form.Events, string(models.HookEventFork))
w.Issues = com.IsSliceContainsStr(form.Events, string(models.HookEventIssues))
w.IssueComment = com.IsSliceContainsStr(form.Events, string(models.HookEventIssueComment))
w.Push = com.IsSliceContainsStr(form.Events, string(models.HookEventPush))
w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
w.Repository = com.IsSliceContainsStr(form.Events, string(models.HookEventRepository))
w.Release = com.IsSliceContainsStr(form.Events, string(models.HookEventRelease))
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.Error(500, "UpdateEvent", err) ctx.Error(500, "UpdateEvent", err)
return false return false

View File

@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/user" userSetting "code.gitea.io/gitea/routers/user/setting"
) )
const ( const (
@ -91,7 +91,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) {
// SettingsAvatar response for change avatar on settings page // SettingsAvatar response for change avatar on settings page
func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) {
form.Source = auth.AvatarLocal form.Source = auth.AvatarLocal
if err := user.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
ctx.Flash.Error(err.Error()) ctx.Flash.Error(err.Error())
} else { } else {
ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success")) ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))

View File

@ -1102,6 +1102,7 @@ func UpdateCommentContent(ctx *context.Context) {
return return
} }
oldContent := comment.Content
comment.Content = ctx.Query("content") comment.Content = ctx.Query("content")
if len(comment.Content) == 0 { if len(comment.Content) == 0 {
ctx.JSON(200, map[string]interface{}{ ctx.JSON(200, map[string]interface{}{
@ -1109,7 +1110,7 @@ func UpdateCommentContent(ctx *context.Context) {
}) })
return return
} }
if err = models.UpdateComment(comment); err != nil { if err = models.UpdateComment(ctx.User, comment, oldContent); err != nil {
ctx.ServerError("UpdateComment", err) ctx.ServerError("UpdateComment", err)
return return
} }
@ -1135,7 +1136,7 @@ func DeleteComment(ctx *context.Context) {
return return
} }
if err = models.DeleteComment(comment); err != nil { if err = models.DeleteComment(ctx.User, comment); err != nil {
ctx.ServerError("DeleteCommentByID", err) ctx.ServerError("DeleteCommentByID", err)
return return
} }

View File

@ -23,9 +23,9 @@ import (
) )
const ( const (
tplHooks base.TplName = "repo/settings/hooks" tplHooks base.TplName = "repo/settings/webhook/base"
tplHookNew base.TplName = "repo/settings/hook_new" tplHookNew base.TplName = "repo/settings/webhook/new"
tplOrgHookNew base.TplName = "org/settings/hook_new" tplOrgHookNew base.TplName = "org/settings/webhook/new"
) )
// Webhooks render web hooks list page // Webhooks render web hooks list page
@ -118,10 +118,15 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
SendEverything: form.SendEverything(), SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(), ChooseEvents: form.ChooseEvents(),
HookEvents: models.HookEvents{ HookEvents: models.HookEvents{
Create: form.Create, Create: form.Create,
Push: form.Push, Delete: form.Delete,
PullRequest: form.PullRequest, Fork: form.Fork,
Repository: form.Repository, Issues: form.Issues,
IssueComment: form.IssueComment,
Release: form.Release,
Push: form.Push,
PullRequest: form.PullRequest,
Repository: form.Repository,
}, },
} }
} }

View File

@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/routers/private" "code.gitea.io/gitea/routers/private"
"code.gitea.io/gitea/routers/repo" "code.gitea.io/gitea/routers/repo"
"code.gitea.io/gitea/routers/user" "code.gitea.io/gitea/routers/user"
userSetting "code.gitea.io/gitea/routers/user/setting"
"github.com/go-macaron/binding" "github.com/go-macaron/binding"
"github.com/go-macaron/cache" "github.com/go-macaron/cache"
@ -216,39 +217,39 @@ func RegisterRoutes(m *macaron.Macaron) {
}, reqSignOut) }, reqSignOut)
m.Group("/user/settings", func() { m.Group("/user/settings", func() {
m.Get("", user.Settings) m.Get("", userSetting.Profile)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost) m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost)
m.Post("/avatar/delete", user.SettingsDeleteAvatar) m.Post("/avatar/delete", userSetting.DeleteAvatar)
m.Group("/account", func() { m.Group("/account", func() {
m.Combo("").Get(user.SettingsAccount).Post(bindIgnErr(auth.ChangePasswordForm{}), user.SettingsAccountPost) m.Combo("").Get(userSetting.Account).Post(bindIgnErr(auth.ChangePasswordForm{}), userSetting.AccountPost)
m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost) m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost)
m.Post("/email/delete", user.DeleteEmail) m.Post("/email/delete", userSetting.DeleteEmail)
m.Post("/delete", user.SettingsDelete) m.Post("/delete", userSetting.DeleteAccount)
}) })
m.Group("/security", func() { m.Group("/security", func() {
m.Get("", user.SettingsSecurity) m.Get("", userSetting.Security)
m.Group("/two_factor", func() { m.Group("/two_factor", func() {
m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch) m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
m.Post("/disable", user.SettingsTwoFactorDisable) m.Post("/disable", userSetting.DisableTwoFactor)
m.Get("/enroll", user.SettingsTwoFactorEnroll) m.Get("/enroll", userSetting.EnrollTwoFactor)
m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost) m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
}) })
m.Group("/openid", func() { m.Group("/openid", func() {
m.Post("", bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost) m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost)
m.Post("/delete", user.DeleteOpenID) m.Post("/delete", userSetting.DeleteOpenID)
m.Post("/toggle_visibility", user.ToggleOpenIDVisibility) m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
}, openIDSignInEnabled) }, openIDSignInEnabled)
m.Post("/account_link", user.SettingsDeleteAccountLink) m.Post("/account_link", userSetting.DeleteAccountLink)
}) })
m.Combo("/applications").Get(user.SettingsApplications). m.Combo("/applications").Get(userSetting.Applications).
Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost) Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost)
m.Post("/applications/delete", user.SettingsDeleteApplication) m.Post("/applications/delete", userSetting.DeleteApplication)
m.Combo("/keys").Get(user.SettingsKeys). m.Combo("/keys").Get(userSetting.Keys).
Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost) Post(bindIgnErr(auth.AddKeyForm{}), userSetting.KeysPost)
m.Post("/keys/delete", user.DeleteKey) m.Post("/keys/delete", userSetting.DeleteKey)
m.Get("/organization", user.SettingsOrganization) m.Get("/organization", userSetting.Organization)
m.Get("/repos", user.SettingsRepos) m.Get("/repos", userSetting.Repos)
// redirects from old settings urls to new ones // redirects from old settings urls to new ones
// TODO: can be removed on next major version // TODO: can be removed on next major version

View File

@ -1,808 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/Unknwon/com"
"github.com/Unknwon/i18n"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"encoding/base64"
"html/template"
"image/png"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
const (
tplSettingsProfile base.TplName = "user/settings/profile"
tplSettingsAccount base.TplName = "user/settings/account"
tplSettingsSecurity base.TplName = "user/settings/security"
tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
tplSettingsApplications base.TplName = "user/settings/applications"
tplSettingsKeys base.TplName = "user/settings/keys"
tplSettingsOrganization base.TplName = "user/settings/organization"
tplSettingsRepositories base.TplName = "user/settings/repos"
)
// Settings render user's profile page
func Settings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
ctx.HTML(200, tplSettingsProfile)
}
func handleUsernameChange(ctx *context.Context, newName string) {
// Non-local users are not allowed to change their username.
if len(newName) == 0 || !ctx.User.IsLocal() {
return
}
// Check if user name has been changed
if ctx.User.LowerName != strings.ToLower(newName) {
if err := models.ChangeUserName(ctx.User, newName); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrEmailAlreadyUsed(err):
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNameReserved(err):
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
default:
ctx.ServerError("ChangeUserName", err)
}
return
}
log.Trace("User name changed: %s -> %s", ctx.User.Name, newName)
}
// In case it's just a case change
ctx.User.Name = newName
ctx.User.LowerName = strings.ToLower(newName)
}
// SettingsPost response for change user's profile
func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
if ctx.HasError() {
ctx.HTML(200, tplSettingsProfile)
return
}
handleUsernameChange(ctx, form.Name)
if ctx.Written() {
return
}
ctx.User.FullName = form.FullName
ctx.User.Email = form.Email
ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
ctx.User.Website = form.Website
ctx.User.Location = form.Location
ctx.User.Language = form.Language
if err := models.UpdateUserSetting(ctx.User); err != nil {
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.ServerError("UpdateUser", err)
return
}
// Update the language to the one we just set
ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
log.Trace("User settings updated: %s", ctx.User.Name)
ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// UpdateAvatarSetting update user's avatar
// FIXME: limit size.
func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error {
ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal
if len(form.Gravatar) > 0 {
ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
ctxUser.AvatarEmail = form.Gravatar
}
if form.Avatar != nil {
fr, err := form.Avatar.Open()
if err != nil {
return fmt.Errorf("Avatar.Open: %v", err)
}
defer fr.Close()
data, err := ioutil.ReadAll(fr)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
}
if !base.IsImageFile(data) {
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
}
if err = ctxUser.UploadAvatar(data); err != nil {
return fmt.Errorf("UploadAvatar: %v", err)
}
} else {
// No avatar is uploaded but setting has been changed to enable,
// generate a random one when needed.
if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) {
if err := ctxUser.GenerateRandomAvatar(); err != nil {
log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
}
}
}
if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
return fmt.Errorf("UpdateUser: %v", err)
}
return nil
}
// SettingsAvatarPost response for change user's avatar request
func SettingsAvatarPost(ctx *context.Context, form auth.AvatarForm) {
if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// SettingsDeleteAvatar render delete avatar page
func SettingsDeleteAvatar(ctx *context.Context) {
if err := ctx.User.DeleteAvatar(); err != nil {
ctx.Flash.Error(err.Error())
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// SettingsAccount renders change user's password, user's email and user suicide page
func SettingsAccount(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.User.Email
emails, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
ctx.Data["Emails"] = emails
ctx.HTML(200, tplSettingsAccount)
}
// SettingsAccountPost response for change user's password
func SettingsAccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if ctx.HasError() {
ctx.HTML(200, tplSettingsAccount)
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
} else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
} else {
var err error
if ctx.User.Salt, err = models.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
ctx.User.HashPassword(form.Password)
if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User password updated: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// SettingsEmailPost response for change user's email
func SettingsEmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
// Make emailaddress primary.
if ctx.Query("_method") == "PRIMARY" {
if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
ctx.ServerError("MakeEmailPrimary", err)
return
}
log.Trace("Email made primary: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Add Email address.
emails, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
ctx.Data["Emails"] = emails
if ctx.HasError() {
ctx.HTML(200, tplSettingsAccount)
return
}
email := &models.EmailAddress{
UID: ctx.User.ID,
Email: form.Email,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
if err := models.AddEmailAddress(email); err != nil {
if models.IsErrEmailAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
return
}
ctx.ServerError("AddEmailAddress", err)
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
models.SendActivateEmailMail(ctx.Context, ctx.User, email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
log.Trace("Email address added: %s", email.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
ctx.ServerError("DeleteEmail", err)
return
}
log.Trace("Email address deleted: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/account",
})
}
// SettingsDelete render user suicide page and response for delete user himself
func SettingsDelete(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
} else {
ctx.ServerError("UserSignIn", err)
}
return
}
if err := models.DeleteUser(ctx.User); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case models.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("form.still_has_org"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
default:
ctx.ServerError("DeleteUser", err)
}
} else {
log.Trace("Account deleted: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/")
}
}
// SettingsSecurity render change user's password page and 2FA
func SettingsSecurity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
enrolled := true
_, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
enrolled = false
} else {
ctx.ServerError("SettingsTwoFactor", err)
return
}
}
ctx.Data["TwofaEnrolled"] = enrolled
accountLinks, err := models.ListAccountLinks(ctx.User)
if err != nil {
ctx.ServerError("ListAccountLinks", err)
return
}
// map the provider display name with the LoginSource
sources := make(map[*models.LoginSource]string)
for _, externalAccount := range accountLinks {
if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
var providerDisplayName string
if loginSource.IsOAuth2() {
providerTechnicalName := loginSource.OAuth2().Provider
providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
} else {
providerDisplayName = loginSource.Name
}
sources[loginSource] = providerDisplayName
}
}
ctx.Data["AccountLinks"] = sources
if ctx.Query("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
openid, err := models.GetUserOpenIDs(ctx.User.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
ctx.HTML(200, tplSettingsSecurity)
}
// SettingsDeleteAccountLink delete a single account link
func SettingsDeleteAccountLink(ctx *context.Context) {
if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}
// SettingsApplications render manage access token page
func SettingsApplications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.HTML(200, tplSettingsApplications)
}
// SettingsApplicationsPost response for add user's access token
func SettingsApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
if ctx.HasError() {
tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.HTML(200, tplSettingsApplications)
return
}
t := &models.AccessToken{
UID: ctx.User.ID,
Name: form.Name,
}
if err := models.NewAccessToken(t); err != nil {
ctx.ServerError("NewAccessToken", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
ctx.Flash.Info(t.Sha1)
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
}
// SettingsDeleteApplication response for delete user access token
func SettingsDeleteApplication(ctx *context.Context) {
if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/applications",
})
}
// SettingsKeys render user's SSH/GPG public keys page
func SettingsKeys(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
ctx.HTML(200, tplSettingsKeys)
}
// SettingsKeysPost response for change user's SSH/GPG keys
func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsKeys"] = true
keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
if ctx.HasError() {
ctx.HTML(200, tplSettingsKeys)
return
}
switch form.Type {
case "gpg":
key, err := models.AddGPGKey(ctx.User.ID, form.Content)
if err != nil {
ctx.Data["HasGPGError"] = true
switch {
case models.IsErrGPGKeyParsing(err):
ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case models.IsErrGPGKeyIDAlreadyUsed(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
case models.IsErrGPGNoEmailFound(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
content, err := models.CheckPublicKeyString(form.Content)
if err != nil {
if models.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else if models.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
ctx.Data["HasSSHError"] = true
switch {
case models.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case models.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
}
// DeleteKey response for delete user's SSH/GPG key
func DeleteKey(ctx *context.Context) {
switch ctx.Query("type") {
case "gpg":
if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
}
case "ssh":
if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
}
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/keys",
})
}
// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code.
func SettingsTwoFactorRegenerateScratch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = t.GenerateScratchToken(); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.UpdateTwoFactor(t); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// SettingsTwoFactorDisable deletes the user's 2FA settings.
func SettingsTwoFactorDisable(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
var otpKey *otp.Key
var err error
uri := ctx.Session.Get("twofaUri")
if uri != nil {
otpKey, err = otp.NewKeyFromURL(uri.(string))
}
if otpKey == nil {
err = nil // clear the error, in case the URL was invalid
otpKey, err = totp.Generate(totp.GenerateOpts{
Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")",
AccountName: ctx.User.Name,
})
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
ctx.Session.Set("twofaSecret", otpKey.Secret())
ctx.Session.Set("twofaUri", otpKey.String())
return true
}
// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA.
func SettingsTwoFactorEnroll(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
}
// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA.
func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
secret := ctx.Session.Get("twofaSecret").(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
t = &models.TwoFactor{
UID: ctx.User.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
err = t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.NewTwoFactor(t); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Session.Delete("twofaSecret")
ctx.Session.Delete("twofaUri")
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// SettingsOrganization render all the organization of the user
func SettingsOrganization(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsOrganization"] = true
orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
if err != nil {
ctx.ServerError("GetOrgsByUserID", err)
return
}
ctx.Data["Orgs"] = orgs
ctx.HTML(200, tplSettingsOrganization)
}
// SettingsRepos display a list of all repositories of the user
func SettingsRepos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsRepos"] = true
ctxUser := ctx.User
var err error
if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
repos := ctxUser.Repos
for i := range repos {
if repos[i].IsFork {
err := repos[i].GetBaseRepo()
if err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
err = repos[i].BaseRepo.GetOwner()
if err != nil {
ctx.ServerError("GetOwner", err)
return
}
}
}
ctx.Data["Owner"] = ctxUser
ctx.Data["Repos"] = repos
ctx.HTML(200, tplSettingsRepositories)
}

View File

@ -0,0 +1,174 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
const (
tplSettingsAccount base.TplName = "user/settings/account"
)
// Account renders change user's password, user's email and user suicide page
func Account(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.User.Email
emails, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
ctx.Data["Emails"] = emails
ctx.HTML(200, tplSettingsAccount)
}
// AccountPost response for change user's password
func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if ctx.HasError() {
ctx.HTML(200, tplSettingsAccount)
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength))
} else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
} else {
var err error
if ctx.User.Salt, err = models.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
ctx.User.HashPassword(form.Password)
if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User password updated: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// EmailPost response for change user's email
func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
// Make emailaddress primary.
if ctx.Query("_method") == "PRIMARY" {
if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil {
ctx.ServerError("MakeEmailPrimary", err)
return
}
log.Trace("Email made primary: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Add Email address.
emails, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
ctx.Data["Emails"] = emails
if ctx.HasError() {
ctx.HTML(200, tplSettingsAccount)
return
}
email := &models.EmailAddress{
UID: ctx.User.ID,
Email: form.Email,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
if err := models.AddEmailAddress(email); err != nil {
if models.IsErrEmailAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
return
}
ctx.ServerError("AddEmailAddress", err)
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
models.SendActivateEmailMail(ctx.Context, ctx.User, email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
log.Trace("Email address added: %s", email.Email)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
ctx.ServerError("DeleteEmail", err)
return
}
log.Trace("Email address deleted: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.email_deletion_success"))
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/account",
})
}
// DeleteAccount render user suicide page and response for delete user himself
func DeleteAccount(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
} else {
ctx.ServerError("UserSignIn", err)
}
return
}
if err := models.DeleteUser(ctx.User); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
case models.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("form.still_has_org"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
default:
ctx.ServerError("DeleteUser", err)
}
} else {
log.Trace("Account deleted: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubURL + "/")
}
}

View File

@ -1,8 +1,8 @@
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package user package setting
import ( import (
"net/http" "net/http"
@ -56,7 +56,7 @@ func TestChangePassword(t *testing.T) {
test.LoadUser(t, ctx, 2) test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1) test.LoadRepo(t, ctx, 1)
SettingsAccountPost(ctx, auth.ChangePasswordForm{ AccountPost(ctx, auth.ChangePasswordForm{
OldPassword: req.OldPassword, OldPassword: req.OldPassword,
Password: req.NewPassword, Password: req.NewPassword,
Retype: req.Retype, Retype: req.Retype,

View File

@ -0,0 +1,77 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
tplSettingsApplications base.TplName = "user/settings/applications"
)
// Applications render manage access token page
func Applications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.HTML(200, tplSettingsApplications)
}
// ApplicationsPost response for add user's access token
func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
if ctx.HasError() {
tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.HTML(200, tplSettingsApplications)
return
}
t := &models.AccessToken{
UID: ctx.User.ID,
Name: form.Name,
}
if err := models.NewAccessToken(t); err != nil {
ctx.ServerError("NewAccessToken", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.generate_token_success"))
ctx.Flash.Info(t.Sha1)
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
}
// DeleteApplication response for delete user access token
func DeleteApplication(ctx *context.Context) {
if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/applications",
})
}

View File

@ -0,0 +1,149 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
tplSettingsKeys base.TplName = "user/settings/keys"
)
// Keys render user's SSH/GPG public keys page
func Keys(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
ctx.HTML(200, tplSettingsKeys)
}
// KeysPost response for change user's SSH/GPG keys
func KeysPost(ctx *context.Context, form auth.AddKeyForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsKeys"] = true
keys, err := models.ListPublicKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
gpgkeys, err := models.ListGPGKeys(ctx.User.ID)
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
if ctx.HasError() {
ctx.HTML(200, tplSettingsKeys)
return
}
switch form.Type {
case "gpg":
key, err := models.AddGPGKey(ctx.User.ID, form.Content)
if err != nil {
ctx.Data["HasGPGError"] = true
switch {
case models.IsErrGPGKeyParsing(err):
ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case models.IsErrGPGKeyIDAlreadyUsed(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
case models.IsErrGPGNoEmailFound(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
content, err := models.CheckPublicKeyString(form.Content)
if err != nil {
if models.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else if models.IsErrKeyUnableVerify(err) {
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil {
ctx.Data["HasSSHError"] = true
switch {
case models.IsErrKeyAlreadyExist(err):
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case models.IsErrKeyNameAlreadyUsed(err):
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
}
// DeleteKey response for delete user's SSH/GPG key
func DeleteKey(ctx *context.Context) {
switch ctx.Query("type") {
case "gpg":
if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
}
case "ssh":
if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
}
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/keys",
})
}

View File

@ -0,0 +1,16 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", "..", ".."))
}

View File

@ -0,0 +1,220 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"errors"
"fmt"
"io/ioutil"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/com"
"github.com/Unknwon/i18n"
)
const (
tplSettingsProfile base.TplName = "user/settings/profile"
tplSettingsOrganization base.TplName = "user/settings/organization"
tplSettingsRepositories base.TplName = "user/settings/repos"
)
// Profile render user's profile page
func Profile(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
ctx.HTML(200, tplSettingsProfile)
}
func handleUsernameChange(ctx *context.Context, newName string) {
// Non-local users are not allowed to change their username.
if len(newName) == 0 || !ctx.User.IsLocal() {
return
}
// Check if user name has been changed
if ctx.User.LowerName != strings.ToLower(newName) {
if err := models.ChangeUserName(ctx.User, newName); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrEmailAlreadyUsed(err):
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNameReserved(err):
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
default:
ctx.ServerError("ChangeUserName", err)
}
return
}
log.Trace("User name changed: %s -> %s", ctx.User.Name, newName)
}
// In case it's just a case change
ctx.User.Name = newName
ctx.User.LowerName = strings.ToLower(newName)
}
// ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
if ctx.HasError() {
ctx.HTML(200, tplSettingsProfile)
return
}
handleUsernameChange(ctx, form.Name)
if ctx.Written() {
return
}
ctx.User.FullName = form.FullName
ctx.User.Email = form.Email
ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
ctx.User.Website = form.Website
ctx.User.Location = form.Location
ctx.User.Language = form.Language
if err := models.UpdateUserSetting(ctx.User); err != nil {
if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.ServerError("UpdateUser", err)
return
}
// Update the language to the one we just set
ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL)
log.Trace("User settings updated: %s", ctx.User.Name)
ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// UpdateAvatarSetting update user's avatar
// FIXME: limit size.
func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error {
ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal
if len(form.Gravatar) > 0 {
ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
ctxUser.AvatarEmail = form.Gravatar
}
if form.Avatar != nil {
fr, err := form.Avatar.Open()
if err != nil {
return fmt.Errorf("Avatar.Open: %v", err)
}
defer fr.Close()
data, err := ioutil.ReadAll(fr)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
}
if !base.IsImageFile(data) {
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
}
if err = ctxUser.UploadAvatar(data); err != nil {
return fmt.Errorf("UploadAvatar: %v", err)
}
} else {
// No avatar is uploaded but setting has been changed to enable,
// generate a random one when needed.
if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) {
if err := ctxUser.GenerateRandomAvatar(); err != nil {
log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
}
}
}
if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
return fmt.Errorf("UpdateUser: %v", err)
}
return nil
}
// AvatarPost response for change user's avatar request
func AvatarPost(ctx *context.Context, form auth.AvatarForm) {
if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// DeleteAvatar render delete avatar page
func DeleteAvatar(ctx *context.Context) {
if err := ctx.User.DeleteAvatar(); err != nil {
ctx.Flash.Error(err.Error())
}
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
// Organization render all the organization of the user
func Organization(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsOrganization"] = true
orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned)
if err != nil {
ctx.ServerError("GetOrgsByUserID", err)
return
}
ctx.Data["Orgs"] = orgs
ctx.HTML(200, tplSettingsOrganization)
}
// Repos display a list of all repositories of the user
func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsRepos"] = true
ctxUser := ctx.User
var err error
if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
repos := ctxUser.Repos
for i := range repos {
if repos[i].IsFork {
err := repos[i].GetBaseRepo()
if err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
err = repos[i].BaseRepo.GetOwner()
if err != nil {
ctx.ServerError("GetOwner", err)
return
}
}
}
ctx.Data["Owner"] = ctxUser
ctx.Data["Repos"] = repos
ctx.HTML(200, tplSettingsRepositories)
}

View File

@ -0,0 +1,92 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
tplSettingsSecurity base.TplName = "user/settings/security"
tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll"
)
// Security render change user's password page and 2FA
func Security(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
enrolled := true
_, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
enrolled = false
} else {
ctx.ServerError("SettingsTwoFactor", err)
return
}
}
ctx.Data["TwofaEnrolled"] = enrolled
tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
accountLinks, err := models.ListAccountLinks(ctx.User)
if err != nil {
ctx.ServerError("ListAccountLinks", err)
return
}
// map the provider display name with the LoginSource
sources := make(map[*models.LoginSource]string)
for _, externalAccount := range accountLinks {
if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
var providerDisplayName string
if loginSource.IsOAuth2() {
providerTechnicalName := loginSource.OAuth2().Provider
providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
} else {
providerDisplayName = loginSource.Name
}
sources[loginSource] = providerDisplayName
}
}
ctx.Data["AccountLinks"] = sources
if ctx.Query("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
openid, err := models.GetUserOpenIDs(ctx.User.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
ctx.HTML(200, tplSettingsSecurity)
}
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
}
ctx.JSON(200, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}

View File

@ -1,8 +1,8 @@
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package user package setting
import ( import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
@ -13,8 +13,8 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
// SettingsOpenIDPost response for change user's openid // OpenIDPost response for change user's openid
func SettingsOpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true

View File

@ -0,0 +1,187 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"bytes"
"encoding/base64"
"html/template"
"image/png"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
func RegenerateScratchTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = t.GenerateScratchToken(); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.UpdateTwoFactor(t); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DisableTwoFactor deletes the user's 2FA settings.
func DisableTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
var otpKey *otp.Key
var err error
uri := ctx.Session.Get("twofaUri")
if uri != nil {
otpKey, err = otp.NewKeyFromURL(uri.(string))
}
if otpKey == nil {
err = nil // clear the error, in case the URL was invalid
otpKey, err = totp.Generate(totp.GenerateOpts{
Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")",
AccountName: ctx.User.Name,
})
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
ctx.Session.Set("twofaSecret", otpKey.Secret())
ctx.Session.Set("twofaUri", otpKey.String())
return true
}
// EnrollTwoFactor shows the page where the user can enroll into 2FA.
func EnrollTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
}
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
secret := ctx.Session.Get("twofaSecret").(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.HTML(200, tplSettingsTwofaEnroll)
return
}
t = &models.TwoFactor{
UID: ctx.User.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
err = t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
if err = models.NewTwoFactor(t); err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Session.Delete("twofaSecret")
ctx.Session.Delete("twofaUri")
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@ -3,7 +3,7 @@
{{template "repo/header" .}} {{template "repo/header" .}}
{{template "repo/settings/navbar" .}} {{template "repo/settings/navbar" .}}
<div class="ui container"> <div class="ui container">
{{template "repo/settings/hook_list" .}} {{template "repo/settings/webhook/list" .}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -6,6 +6,6 @@
<label for="payload_url">{{.i18n.Tr "repo.settings.payload_url"}}</label> <label for="payload_url">{{.i18n.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/hook_settings" .}} {{template "repo/settings/webhook/settings" .}}
</form> </form>
{{end}} {{end}}

View File

@ -14,6 +14,6 @@
<label for="icon_url">{{.i18n.Tr "repo.settings.discord_icon_url"}}</label> <label for="icon_url">{{.i18n.Tr "repo.settings.discord_icon_url"}}</label>
<input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png"> <input id="icon_url" name="icon_url" value="{{.DiscordHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png">
</div> </div>
{{template "repo/settings/hook_settings" .}} {{template "repo/settings/webhook/settings" .}}
</form> </form>
{{end}} {{end}}

View File

@ -23,6 +23,6 @@
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label> <label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label>
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off"> <input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
</div> </div>
{{template "repo/settings/hook_settings" .}} {{template "repo/settings/webhook/settings" .}}
</form> </form>
{{end}} {{end}}

View File

@ -23,6 +23,6 @@
<label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label> <label for="secret">{{.i18n.Tr "repo.settings.secret"}}</label>
<input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off"> <input id="secret" name="secret" type="password" value="{{.Webhook.Secret}}" autocomplete="off">
</div> </div>
{{template "repo/settings/hook_settings" .}} {{template "repo/settings/webhook/settings" .}}
</form> </form>
{{end}} {{end}}

View File

@ -48,4 +48,4 @@
</div> </div>
</div> </div>
{{template "repo/settings/hook_delete_modal" .}} {{template "repo/settings/webhook/delete_modal" .}}

View File

@ -21,14 +21,14 @@
</div> </div>
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
{{template "repo/settings/hook_gitea" .}} {{template "repo/settings/webhook/gitea" .}}
{{template "repo/settings/hook_gogs" .}} {{template "repo/settings/webhook/gogs" .}}
{{template "repo/settings/hook_slack" .}} {{template "repo/settings/webhook/slack" .}}
{{template "repo/settings/hook_discord" .}} {{template "repo/settings/webhook/discord" .}}
{{template "repo/settings/hook_dingtalk" .}} {{template "repo/settings/webhook/dingtalk" .}}
</div> </div>
{{template "repo/settings/hook_history" .}} {{template "repo/settings/webhook/history" .}}
</div> </div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -32,6 +32,26 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Delete -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="delete" type="checkbox" tabindex="0" {{if .Webhook.Delete}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_delete"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_delete_desc"}}</span>
</div>
</div>
</div>
<!-- Fork -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="fork" type="checkbox" tabindex="0" {{if .Webhook.Fork}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_fork"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_fork_desc"}}</span>
</div>
</div>
</div>
<!-- Push --> <!-- Push -->
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
@ -42,6 +62,26 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Issues -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="issues" type="checkbox" tabindex="0" {{if .Webhook.Issues}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_issues"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_issues_desc"}}</span>
</div>
</div>
</div>
<!-- Issue Comment -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="issue_comment" type="checkbox" tabindex="0" {{if .Webhook.IssueComment}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_issue_comment"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_issue_comment_desc"}}</span>
</div>
</div>
</div>
<!-- Pull Request --> <!-- Pull Request -->
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
@ -62,6 +102,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Release -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="release" type="checkbox" tabindex="0" {{if .Webhook.Release}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_release"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_release_desc"}}</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -83,4 +133,4 @@
{{end}} {{end}}
</div> </div>
{{template "repo/settings/hook_delete_modal" .}} {{template "repo/settings/webhook/delete_modal" .}}

View File

@ -23,6 +23,6 @@
<label for="color">{{.i18n.Tr "repo.settings.slack_color"}}</label> <label for="color">{{.i18n.Tr "repo.settings.slack_color"}}</label>
<input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="e.g. #dd4b39, good, warning, danger"> <input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="e.g. #dd4b39, good, warning, danger">
</div> </div>
{{template "repo/settings/hook_settings" .}} {{template "repo/settings/webhook/settings" .}}
</form> </form>
{{end}} {{end}}

View File

@ -2,6 +2,7 @@
<div class="user settings applications"> <div class="user settings applications">
{{template "user/settings/navbar" .}} {{template "user/settings/navbar" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.i18n.Tr "settings.manage_access_token"}} {{.i18n.Tr "settings.manage_access_token"}}
</h4> </h4>

View File

@ -43,7 +43,7 @@
<div class="field"> <div class="field">
<label for="language">{{.i18n.Tr "settings.language"}}</label> <label for="language">{{.i18n.Tr "settings.language"}}</label>
<div class="ui language selection dropdown" id="language"> <div class="ui language selection dropdown" id="language">
<input name="language" type="hidden"> <input name="language" type="hidden" value="{{.SignedUser.Language}}">
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div> <div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
<div class="menu"> <div class="menu">

View File

@ -172,9 +172,14 @@ type PayloadCommitVerification struct {
var ( var (
_ Payloader = &CreatePayload{} _ Payloader = &CreatePayload{}
_ Payloader = &DeletePayload{}
_ Payloader = &ForkPayload{}
_ Payloader = &PushPayload{} _ Payloader = &PushPayload{}
_ Payloader = &IssuePayload{} _ Payloader = &IssuePayload{}
_ Payloader = &IssueCommentPayload{}
_ Payloader = &PullRequestPayload{} _ Payloader = &PullRequestPayload{}
_ Payloader = &RepositoryPayload{}
_ Payloader = &ReleasePayload{}
) )
// _________ __ // _________ __
@ -224,6 +229,123 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
return hook, nil return hook, nil
} }
// ________ .__ __
// \______ \ ____ | | _____/ |_ ____
// | | \_/ __ \| | _/ __ \ __\/ __ \
// | ` \ ___/| |_\ ___/| | \ ___/
// /_______ /\___ >____/\___ >__| \___ >
// \/ \/ \/ \/
// PusherType define the type to push
type PusherType string
// describe all the PusherTypes
const (
PusherTypeUser PusherType = "user"
)
// DeletePayload represents delete payload
type DeletePayload struct {
Ref string `json:"ref"`
RefType string `json:"ref_type"`
PusherType PusherType `json:"pusher_type"`
Repo *Repository `json:"repository"`
Sender *User `json:"sender"`
}
// SetSecret implements Payload
func (p *DeletePayload) SetSecret(secret string) {
}
// JSONPayload implements Payload
func (p *DeletePayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// ___________ __
// \_ _____/__________| | __
// | __)/ _ \_ __ \ |/ /
// | \( <_> ) | \/ <
// \___ / \____/|__| |__|_ \
// \/ \/
// ForkPayload represents fork payload
type ForkPayload struct {
Forkee *Repository `json:"forkee"`
Repo *Repository `json:"repository"`
Sender *User `json:"sender"`
}
// SetSecret implements Payload
func (p *ForkPayload) SetSecret(secret string) {
}
// JSONPayload implements Payload
func (p *ForkPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// HookIssueCommentAction defines hook issue comment action
type HookIssueCommentAction string
// all issue comment actions
const (
HookIssueCommentCreated HookIssueCommentAction = "created"
HookIssueCommentEdited HookIssueCommentAction = "edited"
HookIssueCommentDeleted HookIssueCommentAction = "deleted"
)
// IssueCommentPayload represents a payload information of issue comment event.
type IssueCommentPayload struct {
Action HookIssueCommentAction `json:"action"`
Issue *Issue `json:"issue"`
Comment *Comment `json:"comment"`
Changes *ChangesPayload `json:"changes,omitempty"`
Repository *Repository `json:"repository"`
Sender *User `json:"sender"`
}
// SetSecret implements Payload
func (p *IssueCommentPayload) SetSecret(secret string) {
}
// JSONPayload implements Payload
func (p *IssueCommentPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// __________ .__
// \______ \ ____ | | ____ _____ ______ ____
// | _// __ \| | _/ __ \\__ \ / ___// __ \
// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/
// |____|_ /\___ >____/\___ >____ /____ >\___ >
// \/ \/ \/ \/ \/ \/
// HookReleaseAction defines hook release action type
type HookReleaseAction string
// all release actions
const (
HookReleasePublished HookReleaseAction = "published"
)
// ReleasePayload represents a payload information of release event.
type ReleasePayload struct {
Action HookReleaseAction `json:"action"`
Release *Release `json:"release"`
Repository *Repository `json:"repository"`
Sender *User `json:"sender"`
}
// SetSecret implements Payload
func (p *ReleasePayload) SetSecret(secret string) {
}
// JSONPayload implements Payload
func (p *ReleasePayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// __________ .__ // __________ .__
// \______ \__ __ _____| |__ // \______ \__ __ _____| |__
// | ___/ | \/ ___/ | \ // | ___/ | \/ ___/ | \

View File

@ -118,14 +118,14 @@ func (c *Client) CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue,
// EditIssueOption options for editing an issue // EditIssueOption options for editing an issue
type EditIssueOption struct { type EditIssueOption struct {
Title string `json:"title"` Title string `json:"title"`
Body *string `json:"body"` Body *string `json:"body"`
Assignee *string `json:"assignee"` Assignee *string `json:"assignee"`
Assignees []string `json:"assignees"` Assignees []string `json:"assignees"`
Milestone *int64 `json:"milestone"` Milestone *int64 `json:"milestone"`
State *string `json:"state"` State *string `json:"state"`
// swagger:strfmt date-time // swagger:strfmt date-time
Deadline *time.Time `json:"due_date"` Deadline *time.Time `json:"due_date"`
} }
// EditIssue modify an existing issue for a given repository // EditIssue modify an existing issue for a given repository
@ -138,3 +138,17 @@ func (c *Client) EditIssue(owner, repo string, index int64, opt EditIssueOption)
return issue, c.getParsedResponse("PATCH", fmt.Sprintf("/repos/%s/%s/issues/%d", owner, repo, index), return issue, c.getParsedResponse("PATCH", fmt.Sprintf("/repos/%s/%s/issues/%d", owner, repo, index),
jsonHeader, bytes.NewReader(body), issue) jsonHeader, bytes.NewReader(body), issue)
} }
// EditDeadlineOption options for creating a deadline
type EditDeadlineOption struct {
// required:true
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
}
// IssueDeadline represents an issue deadline
// swagger:model
type IssueDeadline struct {
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
}

View File

@ -85,16 +85,16 @@ func (c *Client) GetPullRequest(owner, repo string, index int64) (*PullRequest,
// CreatePullRequestOption options when creating a pull request // CreatePullRequestOption options when creating a pull request
type CreatePullRequestOption struct { type CreatePullRequestOption struct {
Head string `json:"head" binding:"Required"` Head string `json:"head" binding:"Required"`
Base string `json:"base" binding:"Required"` Base string `json:"base" binding:"Required"`
Title string `json:"title" binding:"Required"` Title string `json:"title" binding:"Required"`
Body string `json:"body"` Body string `json:"body"`
Assignee string `json:"assignee"` Assignee string `json:"assignee"`
Assignees []string `json:"assignees"` Assignees []string `json:"assignees"`
Milestone int64 `json:"milestone"` Milestone int64 `json:"milestone"`
Labels []int64 `json:"labels"` Labels []int64 `json:"labels"`
// swagger:strfmt date-time // swagger:strfmt date-time
Deadline *time.Time `json:"due_date"` Deadline *time.Time `json:"due_date"`
} }
// CreatePullRequest create pull request with options // CreatePullRequest create pull request with options
@ -110,15 +110,15 @@ func (c *Client) CreatePullRequest(owner, repo string, opt CreatePullRequestOpti
// EditPullRequestOption options when modify pull request // EditPullRequestOption options when modify pull request
type EditPullRequestOption struct { type EditPullRequestOption struct {
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` Body string `json:"body"`
Assignee string `json:"assignee"` Assignee string `json:"assignee"`
Assignees []string `json:"assignees"` Assignees []string `json:"assignees"`
Milestone int64 `json:"milestone"` Milestone int64 `json:"milestone"`
Labels []int64 `json:"labels"` Labels []int64 `json:"labels"`
State *string `json:"state"` State *string `json:"state"`
// swagger:strfmt date-time // swagger:strfmt date-time
Deadline *time.Time `json:"due_date"` Deadline *time.Time `json:"due_date"`
} }
// EditPullRequest modify pull request with PR id and options // EditPullRequest modify pull request with PR id and options

6
vendor/vendor.json vendored
View File

@ -9,10 +9,10 @@
"revisionTime": "2018-05-17T01:19:24Z" "revisionTime": "2018-05-17T01:19:24Z"
}, },
{ {
"checksumSHA1": "WMD6+Qh2+5hd9uiq910pF/Ihylw=", "checksumSHA1": "LnxY/6xD4h9dCCJ5nxKEfZZs1Vk=",
"path": "code.gitea.io/sdk/gitea", "path": "code.gitea.io/sdk/gitea",
"revision": "1c8d12f79a51605ed91587aa6b86cf38fc0f987f", "revision": "7fa627fa5d67d18c39d6dd3c6c4db836916bf234",
"revisionTime": "2018-05-01T11:15:19Z" "revisionTime": "2018-05-10T12:54:05Z"
}, },
{ {
"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", "checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=",