1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Refactor routers directory (#15800)

* refactor routers directory

* move func used for web and api to common

* make corsHandler a function to prohibit side efects

* rm unused func

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
Lunny Xiao
2021-06-09 07:33:54 +08:00
committed by GitHub
parent e03a91a48e
commit 1bfb0a24d8
107 changed files with 940 additions and 800 deletions

485
routers/web/admin/admin.go Normal file
View File

@@ -0,0 +1,485 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 admin
import (
"fmt"
"net/http"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/cron"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
jsoniter "github.com/json-iterator/go"
"gitea.com/go-chi/session"
)
const (
tplDashboard base.TplName = "admin/dashboard"
tplConfig base.TplName = "admin/config"
tplMonitor base.TplName = "admin/monitor"
tplQueue base.TplName = "admin/queue"
)
var sysStatus struct {
Uptime string
NumGoroutine int
// General statistics.
MemAllocated string // bytes allocated and still in use
MemTotal string // bytes allocated (even if freed)
MemSys string // bytes obtained from system (sum of XxxSys below)
Lookups uint64 // number of pointer lookups
MemMallocs uint64 // number of mallocs
MemFrees uint64 // number of frees
// Main allocation heap statistics.
HeapAlloc string // bytes allocated and still in use
HeapSys string // bytes obtained from system
HeapIdle string // bytes in idle spans
HeapInuse string // bytes in non-idle span
HeapReleased string // bytes released to the OS
HeapObjects uint64 // total number of allocated objects
// Low-level fixed-size structure allocator statistics.
// Inuse is bytes used now.
// Sys is bytes obtained from system.
StackInuse string // bootstrap stacks
StackSys string
MSpanInuse string // mspan structures
MSpanSys string
MCacheInuse string // mcache structures
MCacheSys string
BuckHashSys string // profiling bucket hash table
GCSys string // GC metadata
OtherSys string // other system allocations
// Garbage collector statistics.
NextGC string // next run in HeapAlloc time (bytes)
LastGC string // last run in absolute time (ns)
PauseTotalNs string
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
NumGC uint32
}
func updateSystemStatus() {
sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, "en")
m := new(runtime.MemStats)
runtime.ReadMemStats(m)
sysStatus.NumGoroutine = runtime.NumGoroutine()
sysStatus.MemAllocated = base.FileSize(int64(m.Alloc))
sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc))
sysStatus.MemSys = base.FileSize(int64(m.Sys))
sysStatus.Lookups = m.Lookups
sysStatus.MemMallocs = m.Mallocs
sysStatus.MemFrees = m.Frees
sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc))
sysStatus.HeapSys = base.FileSize(int64(m.HeapSys))
sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle))
sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse))
sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased))
sysStatus.HeapObjects = m.HeapObjects
sysStatus.StackInuse = base.FileSize(int64(m.StackInuse))
sysStatus.StackSys = base.FileSize(int64(m.StackSys))
sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse))
sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys))
sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse))
sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys))
sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys))
sysStatus.GCSys = base.FileSize(int64(m.GCSys))
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
sysStatus.NumGC = m.NumGC
}
// Dashboard show admin panel dashboard
func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.Data["SSH"] = setting.SSH
ctx.HTML(http.StatusOK, tplDashboard)
}
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
// Run operation.
if form.Op != "" {
task := cron.GetTask(form.Op)
if task != nil {
go task.RunWithUser(ctx.User, nil)
ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
} else {
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))
}
}
if form.From == "monitor" {
ctx.Redirect(setting.AppSubURL + "/admin/monitor")
} else {
ctx.Redirect(setting.AppSubURL + "/admin")
}
}
// SendTestMail send test mail to confirm mail service is OK
func SendTestMail(ctx *context.Context) {
email := ctx.Query("email")
// Send a test email to the user's email address and redirect back to Config
if err := mailer.SendTestMail(email); err != nil {
ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err))
} else {
ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email))
}
ctx.Redirect(setting.AppSubURL + "/admin/config")
}
func shadowPasswordKV(cfgItem, splitter string) string {
fields := strings.Split(cfgItem, splitter)
for i := 0; i < len(fields); i++ {
if strings.HasPrefix(fields[i], "password=") {
fields[i] = "password=******"
break
}
}
return strings.Join(fields, splitter)
}
func shadowURL(provider, cfgItem string) string {
u, err := url.Parse(cfgItem)
if err != nil {
log.Error("Shadowing Password for %v failed: %v", provider, err)
return cfgItem
}
if u.User != nil {
atIdx := strings.Index(cfgItem, "@")
if atIdx > 0 {
colonIdx := strings.LastIndex(cfgItem[:atIdx], ":")
if colonIdx > 0 {
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
}
}
}
return cfgItem
}
func shadowPassword(provider, cfgItem string) string {
switch provider {
case "redis":
return shadowPasswordKV(cfgItem, ",")
case "mysql":
//root:@tcp(localhost:3306)/macaron?charset=utf8
atIdx := strings.Index(cfgItem, "@")
if atIdx > 0 {
colonIdx := strings.Index(cfgItem[:atIdx], ":")
if colonIdx > 0 {
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
}
}
return cfgItem
case "postgres":
// user=jiahuachen dbname=macaron port=5432 sslmode=disable
if !strings.HasPrefix(cfgItem, "postgres://") {
return shadowPasswordKV(cfgItem, " ")
}
fallthrough
case "couchbase":
return shadowURL(provider, cfgItem)
// postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
// Notice: use shadowURL
}
return cfgItem
}
// Config show admin config page
func Config(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.config")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminConfig"] = true
ctx.Data["CustomConf"] = setting.CustomConf
ctx.Data["AppUrl"] = setting.AppURL
ctx.Data["Domain"] = setting.Domain
ctx.Data["OfflineMode"] = setting.OfflineMode
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
ctx.Data["RunUser"] = setting.RunUser
ctx.Data["RunMode"] = strings.Title(setting.RunMode)
if version, err := git.LocalVersion(); err == nil {
ctx.Data["GitVersion"] = version.Original()
}
ctx.Data["RepoRootPath"] = setting.RepoRootPath
ctx.Data["CustomRootPath"] = setting.CustomPath
ctx.Data["StaticRootPath"] = setting.StaticRootPath
ctx.Data["LogRootPath"] = setting.LogRootPath
ctx.Data["ScriptType"] = setting.ScriptType
ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser
ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail
ctx.Data["SSH"] = setting.SSH
ctx.Data["LFS"] = setting.LFS
ctx.Data["Service"] = setting.Service
ctx.Data["DbCfg"] = setting.Database
ctx.Data["Webhook"] = setting.Webhook
ctx.Data["MailerEnabled"] = false
if setting.MailService != nil {
ctx.Data["MailerEnabled"] = true
ctx.Data["Mailer"] = setting.MailService
}
ctx.Data["CacheAdapter"] = setting.CacheService.Adapter
ctx.Data["CacheInterval"] = setting.CacheService.Interval
ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn)
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
sessionCfg := setting.SessionConfig
if sessionCfg.Provider == "VirtualSession" {
var realSession session.Options
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil {
log.Error("Unable to unmarshall session config for virtualed provider config: %s\nError: %v", sessionCfg.ProviderConfig, err)
}
sessionCfg.Provider = realSession.Provider
sessionCfg.ProviderConfig = realSession.ProviderConfig
sessionCfg.CookieName = realSession.CookieName
sessionCfg.CookiePath = realSession.CookiePath
sessionCfg.Gclifetime = realSession.Gclifetime
sessionCfg.Maxlifetime = realSession.Maxlifetime
sessionCfg.Secure = realSession.Secure
sessionCfg.Domain = realSession.Domain
}
sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig)
ctx.Data["SessionConfig"] = sessionCfg
ctx.Data["DisableGravatar"] = setting.DisableGravatar
ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar
ctx.Data["Git"] = setting.Git
type envVar struct {
Name, Value string
}
envVars := map[string]*envVar{}
if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
}
if len(os.Getenv("GITEA_CUSTOM")) > 0 {
envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
}
ctx.Data["EnvVars"] = envVars
ctx.Data["Loggers"] = setting.GetLogDescriptions()
ctx.Data["EnableAccessLog"] = setting.EnableAccessLog
ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
ctx.Data["EnableXORMLog"] = setting.EnableXORMLog
ctx.Data["LogSQL"] = setting.Database.LogSQL
ctx.HTML(http.StatusOK, tplConfig)
}
// Monitor show admin monitor page
func Monitor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminMonitor"] = true
ctx.Data["Processes"] = process.GetManager().Processes()
ctx.Data["Entries"] = cron.ListTasks()
ctx.Data["Queues"] = queue.GetManager().ManagedQueues()
ctx.HTML(http.StatusOK, tplMonitor)
}
// MonitorCancel cancels a process
func MonitorCancel(ctx *context.Context) {
pid := ctx.ParamsInt64("pid")
process.GetManager().Cancel(pid)
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/monitor",
})
}
// Queue shows details for a specific queue
func Queue(ctx *context.Context) {
qid := ctx.ParamsInt64("qid")
mq := queue.GetManager().GetManagedQueue(qid)
if mq == nil {
ctx.Status(404)
return
}
ctx.Data["Title"] = ctx.Tr("admin.monitor.queue", mq.Name)
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminMonitor"] = true
ctx.Data["Queue"] = mq
ctx.HTML(http.StatusOK, tplQueue)
}
// WorkerCancel cancels a worker group
func WorkerCancel(ctx *context.Context) {
qid := ctx.ParamsInt64("qid")
mq := queue.GetManager().GetManagedQueue(qid)
if mq == nil {
ctx.Status(404)
return
}
pid := ctx.ParamsInt64("pid")
mq.CancelWorkers(pid)
ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.cancelling"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10),
})
}
// Flush flushes a queue
func Flush(ctx *context.Context) {
qid := ctx.ParamsInt64("qid")
mq := queue.GetManager().GetManagedQueue(qid)
if mq == nil {
ctx.Status(404)
return
}
timeout, err := time.ParseDuration(ctx.Query("timeout"))
if err != nil {
timeout = -1
}
ctx.Flash.Info(ctx.Tr("admin.monitor.queue.pool.flush.added", mq.Name))
go func() {
err := mq.Flush(timeout)
if err != nil {
log.Error("Flushing failure for %s: Error %v", mq.Name, err)
}
}()
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
}
// AddWorkers adds workers to a worker group
func AddWorkers(ctx *context.Context) {
qid := ctx.ParamsInt64("qid")
mq := queue.GetManager().GetManagedQueue(qid)
if mq == nil {
ctx.Status(404)
return
}
number := ctx.QueryInt("number")
if number < 1 {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.mustnumbergreaterzero"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
timeout, err := time.ParseDuration(ctx.Query("timeout"))
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.addworkers.musttimeoutduration"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
if _, ok := mq.Managed.(queue.ManagedPool); !ok {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
mq.AddWorkers(number, timeout)
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.pool.added"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
}
// SetQueueSettings sets the maximum number of workers and other settings for this queue
func SetQueueSettings(ctx *context.Context) {
qid := ctx.ParamsInt64("qid")
mq := queue.GetManager().GetManagedQueue(qid)
if mq == nil {
ctx.Status(404)
return
}
if _, ok := mq.Managed.(queue.ManagedPool); !ok {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.pool.none"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
maxNumberStr := ctx.Query("max-number")
numberStr := ctx.Query("number")
timeoutStr := ctx.Query("timeout")
var err error
var maxNumber, number int
var timeout time.Duration
if len(maxNumberStr) > 0 {
maxNumber, err = strconv.Atoi(maxNumberStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
if maxNumber < -1 {
maxNumber = -1
}
} else {
maxNumber = mq.MaxNumberOfWorkers()
}
if len(numberStr) > 0 {
number, err = strconv.Atoi(numberStr)
if err != nil || number < 0 {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.numberworkers.error"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
} else {
number = mq.BoostWorkers()
}
if len(timeoutStr) > 0 {
timeout, err = time.ParseDuration(timeoutStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.timeout.error"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
} else {
timeout = mq.BoostTimeout()
}
mq.SetPoolSettings(maxNumber, number, timeout)
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 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 admin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestShadowPassword(t *testing.T) {
var kases = []struct {
Provider string
CfgItem string
Result string
}{
{
Provider: "redis",
CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180",
Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180",
},
{
Provider: "mysql",
CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8",
Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8",
},
{
Provider: "mysql",
CfgItem: "/gitea?charset=utf8",
Result: "/gitea?charset=utf8",
},
{
Provider: "mysql",
CfgItem: "user:mypassword@/dbname",
Result: "user:******@/dbname",
},
{
Provider: "postgres",
CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full",
Result: "user=pqgotest dbname=pqgotest sslmode=verify-full",
},
{
Provider: "postgres",
CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full",
Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full",
},
{
Provider: "postgres",
CfgItem: "postgres://user:pass@hostname/dbname",
Result: "postgres://user:******@hostname/dbname",
},
{
Provider: "couchbase",
CfgItem: "http://dev-couchbase.example.com:8091/",
Result: "http://dev-couchbase.example.com:8091/",
},
{
Provider: "couchbase",
CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/",
Result: "http://user:******@dev-couchbase.example.com:8091/",
},
}
for _, k := range kases {
assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
}
}

410
routers/web/admin/auths.go Normal file
View File

@@ -0,0 +1,410 @@
// 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 admin
import (
"errors"
"fmt"
"net/http"
"regexp"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/ldap"
"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/auth/pam"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"xorm.io/xorm/convert"
)
const (
tplAuths base.TplName = "admin/auth/list"
tplAuthNew base.TplName = "admin/auth/new"
tplAuthEdit base.TplName = "admin/auth/edit"
)
var (
separatorAntiPattern = regexp.MustCompile(`[^\w-\.]`)
langCodePattern = regexp.MustCompile(`^[a-z]{2}-[A-Z]{2}$`)
)
// Authentications show authentication config page
func Authentications(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.authentication")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminAuthentications"] = true
var err error
ctx.Data["Sources"], err = models.LoginSources()
if err != nil {
ctx.ServerError("LoginSources", err)
return
}
ctx.Data["Total"] = models.CountLoginSources()
ctx.HTML(http.StatusOK, tplAuths)
}
type dropdownItem struct {
Name string
Type interface{}
}
var (
authSources = func() []dropdownItem {
items := []dropdownItem{
{models.LoginNames[models.LoginLDAP], models.LoginLDAP},
{models.LoginNames[models.LoginDLDAP], models.LoginDLDAP},
{models.LoginNames[models.LoginSMTP], models.LoginSMTP},
{models.LoginNames[models.LoginOAuth2], models.LoginOAuth2},
{models.LoginNames[models.LoginSSPI], models.LoginSSPI},
}
if pam.Supported {
items = append(items, dropdownItem{models.LoginNames[models.LoginPAM], models.LoginPAM})
}
return items
}()
securityProtocols = []dropdownItem{
{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
{models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
{models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
}
)
// NewAuthSource render adding a new auth source page
func NewAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["type"] = models.LoginLDAP
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
ctx.Data["smtp_auth"] = "PLAIN"
ctx.Data["is_active"] = true
ctx.Data["is_sync_enabled"] = true
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = models.SMTPAuths
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
ctx.Data["SSPIAutoCreateUsers"] = true
ctx.Data["SSPIAutoActivateUsers"] = true
ctx.Data["SSPIStripDomainNames"] = true
ctx.Data["SSPISeparatorReplacement"] = "_"
ctx.Data["SSPIDefaultLanguage"] = ""
// only the first as default
for key := range models.OAuth2Providers {
ctx.Data["oauth2_provider"] = key
break
}
ctx.HTML(http.StatusOK, tplAuthNew)
}
func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
var pageSize uint32
if form.UsePagedSearch {
pageSize = uint32(form.SearchPageSize)
}
return &models.LDAPConfig{
Source: &ldap.Source{
Name: form.Name,
Host: form.Host,
Port: form.Port,
SecurityProtocol: ldap.SecurityProtocol(form.SecurityProtocol),
SkipVerify: form.SkipVerify,
BindDN: form.BindDN,
UserDN: form.UserDN,
BindPassword: form.BindPassword,
UserBase: form.UserBase,
AttributeUsername: form.AttributeUsername,
AttributeName: form.AttributeName,
AttributeSurname: form.AttributeSurname,
AttributeMail: form.AttributeMail,
AttributesInBind: form.AttributesInBind,
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
SearchPageSize: pageSize,
Filter: form.Filter,
GroupsEnabled: form.GroupsEnabled,
GroupDN: form.GroupDN,
GroupFilter: form.GroupFilter,
GroupMemberUID: form.GroupMemberUID,
UserUID: form.UserUID,
AdminFilter: form.AdminFilter,
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
},
}
}
func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
return &models.SMTPConfig{
Auth: form.SMTPAuth,
Host: form.SMTPHost,
Port: form.SMTPPort,
AllowedDomains: form.AllowedDomains,
TLS: form.TLS,
SkipVerify: form.SkipVerify,
}
}
func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
var customURLMapping *oauth2.CustomURLMapping
if form.Oauth2UseCustomURL {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: form.Oauth2TokenURL,
AuthURL: form.Oauth2AuthURL,
ProfileURL: form.Oauth2ProfileURL,
EmailURL: form.Oauth2EmailURL,
}
} else {
customURLMapping = nil
}
return &models.OAuth2Config{
Provider: form.Oauth2Provider,
ClientID: form.Oauth2Key,
ClientSecret: form.Oauth2Secret,
OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL,
CustomURLMapping: customURLMapping,
IconURL: form.Oauth2IconURL,
}
}
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
return nil, errors.New(ctx.Tr("form.lang_select_error"))
}
return &models.SSPIConfig{
AutoCreateUsers: form.SSPIAutoCreateUsers,
AutoActivateUsers: form.SSPIAutoActivateUsers,
StripDomainNames: form.SSPIStripDomainNames,
SeparatorReplacement: form.SSPISeparatorReplacement,
DefaultLanguage: form.SSPIDefaultLanguage,
}, nil
}
// NewAuthSourcePost response for adding an auth source
func NewAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.new")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
ctx.Data["AuthSources"] = authSources
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = models.SMTPAuths
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
ctx.Data["SSPIAutoCreateUsers"] = true
ctx.Data["SSPIAutoActivateUsers"] = true
ctx.Data["SSPIStripDomainNames"] = true
ctx.Data["SSPISeparatorReplacement"] = "_"
ctx.Data["SSPIDefaultLanguage"] = ""
hasTLS := false
var config convert.Conversion
switch models.LoginType(form.Type) {
case models.LoginLDAP, models.LoginDLDAP:
config = parseLDAPConfig(form)
hasTLS = ldap.SecurityProtocol(form.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
case models.LoginSMTP:
config = parseSMTPConfig(form)
hasTLS = true
case models.LoginPAM:
config = &models.PAMConfig{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
}
case models.LoginOAuth2:
config = parseOAuth2Config(form)
case models.LoginSSPI:
var err error
config, err = parseSSPIConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
return
}
existing, err := models.LoginSourcesByType(models.LoginSSPI)
if err != nil || len(existing) > 0 {
ctx.Data["Err_Type"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
return
}
default:
ctx.Error(http.StatusBadRequest)
return
}
ctx.Data["HasTLS"] = hasTLS
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthNew)
return
}
if err := models.CreateLoginSource(&models.LoginSource{
Type: models.LoginType(form.Type),
Name: form.Name,
IsActived: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
Cfg: config,
}); err != nil {
if models.IsErrLoginSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), tplAuthNew, form)
} else {
ctx.ServerError("CreateSource", err)
}
return
}
log.Trace("Authentication created by admin(%s): %s", ctx.User.Name, form.Name)
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
ctx.Redirect(setting.AppSubURL + "/admin/auths")
}
// EditAuthSource render editing auth source page
func EditAuthSource(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SecurityProtocols"] = securityProtocols
ctx.Data["SMTPAuths"] = models.SMTPAuths
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("GetLoginSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
if source.IsOAuth2() {
ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
}
ctx.HTML(http.StatusOK, tplAuthEdit)
}
// EditAuthSourcePost response for editing auth source
func EditAuthSourcePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AuthenticationForm)
ctx.Data["Title"] = ctx.Tr("admin.auths.edit")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminAuthentications"] = true
ctx.Data["SMTPAuths"] = models.SMTPAuths
ctx.Data["OAuth2Providers"] = models.OAuth2Providers
ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("GetLoginSourceByID", err)
return
}
ctx.Data["Source"] = source
ctx.Data["HasTLS"] = source.HasTLS()
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplAuthEdit)
return
}
var config convert.Conversion
switch models.LoginType(form.Type) {
case models.LoginLDAP, models.LoginDLDAP:
config = parseLDAPConfig(form)
case models.LoginSMTP:
config = parseSMTPConfig(form)
case models.LoginPAM:
config = &models.PAMConfig{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
}
case models.LoginOAuth2:
config = parseOAuth2Config(form)
case models.LoginSSPI:
config, err = parseSSPIConfig(ctx, form)
if err != nil {
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
return
}
default:
ctx.Error(http.StatusBadRequest)
return
}
source.Name = form.Name
source.IsActived = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
if err := models.UpdateSource(source); err != nil {
if models.IsErrOpenIDConnectInitialize(err) {
ctx.Flash.Error(err.Error(), true)
ctx.HTML(http.StatusOK, tplAuthEdit)
} else {
ctx.ServerError("UpdateSource", err)
}
return
}
log.Trace("Authentication changed by admin(%s): %d", ctx.User.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + fmt.Sprint(form.ID))
}
// DeleteAuthSource response for deleting an auth source
func DeleteAuthSource(ctx *context.Context) {
source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
if err != nil {
ctx.ServerError("GetLoginSourceByID", err)
return
}
if err = models.DeleteSource(source); err != nil {
if models.IsErrLoginSourceInUse(err) {
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used"))
} else {
ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/auths/" + ctx.Params(":authid"),
})
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/auths",
})
}

156
routers/web/admin/emails.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package admin
import (
"bytes"
"net/http"
"net/url"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
const (
tplEmails base.TplName = "admin/emails/list"
)
// Emails show all emails
func Emails(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.emails")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminEmails"] = true
opts := &models.SearchEmailOptions{
ListOptions: models.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.QueryInt("page"),
},
}
if opts.Page <= 1 {
opts.Page = 1
}
type ActiveEmail struct {
models.SearchEmailResult
CanChange bool
}
var (
baseEmails []*models.SearchEmailResult
emails []ActiveEmail
count int64
err error
orderBy models.SearchEmailOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "email":
orderBy = models.SearchEmailOrderByEmail
case "reverseemail":
orderBy = models.SearchEmailOrderByEmailReverse
case "username":
orderBy = models.SearchEmailOrderByName
case "reverseusername":
orderBy = models.SearchEmailOrderByNameReverse
default:
ctx.Data["SortType"] = "email"
orderBy = models.SearchEmailOrderByEmail
}
opts.Keyword = ctx.QueryTrim("q")
opts.SortType = orderBy
if len(ctx.Query("is_activated")) != 0 {
opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
}
if len(ctx.Query("is_primary")) != 0 {
opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
baseEmails, count, err = models.SearchEmails(opts)
if err != nil {
ctx.ServerError("SearchEmails", err)
return
}
emails = make([]ActiveEmail, len(baseEmails))
for i := range baseEmails {
emails[i].SearchEmailResult = *baseEmails[i]
// Don't let the admin deactivate its own primary email address
// We already know the user is admin
emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
}
}
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Emails"] = emails
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplEmails)
}
var (
nullByte = []byte{0x00}
)
func isKeywordValid(keyword string) bool {
return !bytes.Contains([]byte(keyword), nullByte)
}
// ActivateEmail serves a POST request for activating/deactivating a user's email
func ActivateEmail(ctx *context.Context) {
truefalse := map[string]bool{"1": true, "0": false}
uid := ctx.QueryInt64("uid")
email := ctx.Query("email")
primary, okp := truefalse[ctx.Query("primary")]
activate, oka := truefalse[ctx.Query("activate")]
if uid == 0 || len(email) == 0 || !okp || !oka {
ctx.Error(http.StatusBadRequest)
return
}
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
if models.IsErrEmailAlreadyUsed(err) {
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
} else {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
}
} else {
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
}
redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
q := url.Values{}
if val := ctx.QueryTrim("q"); len(val) > 0 {
q.Set("q", val)
}
if val := ctx.QueryTrim("sort"); len(val) > 0 {
q.Set("sort", val)
}
if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
q.Set("is_primary", val)
}
if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
q.Set("is_activated", val)
}
redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String())
}

View File

@@ -0,0 +1,72 @@
// 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 admin
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
// tplAdminHooks template path to render hook settings
tplAdminHooks base.TplName = "admin/hooks"
)
// DefaultOrSystemWebhooks renders both admin default and system webhook list pages
func DefaultOrSystemWebhooks(ctx *context.Context) {
var err error
ctx.Data["PageIsAdminSystemHooks"] = true
ctx.Data["PageIsAdminDefaultHooks"] = true
def := make(map[string]interface{}, len(ctx.Data))
sys := make(map[string]interface{}, len(ctx.Data))
for k, v := range ctx.Data {
def[k] = v
sys[k] = v
}
sys["Title"] = ctx.Tr("admin.systemhooks")
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
sys["Webhooks"], err = models.GetSystemWebhooks()
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
if err != nil {
ctx.ServerError("GetWebhooksAdmin", err)
return
}
def["Title"] = ctx.Tr("admin.defaulthooks")
def["Description"] = ctx.Tr("admin.defaulthooks.desc")
def["Webhooks"], err = models.GetDefaultWebhooks()
def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
if err != nil {
ctx.ServerError("GetWebhooksAdmin", err)
return
}
ctx.Data["DefaultWebhooks"] = def
ctx.Data["SystemWebhooks"] = sys
ctx.HTML(http.StatusOK, tplAdminHooks)
}
// DeleteDefaultOrSystemWebhook handler to delete an admin-defined system or default webhook
func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
if err := models.DeleteDefaultSystemWebhook(ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteDefaultWebhook: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/hooks",
})
}

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 admin
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", "..", ".."))
}

View File

@@ -0,0 +1,79 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 admin
import (
"net/http"
"strconv"
"code.gitea.io/gitea/models"
"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 (
tplNotices base.TplName = "admin/notice"
)
// Notices show notices for admin
func Notices(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.notices")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminNotices"] = true
total := models.CountNotices()
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
notices, err := models.Notices(page, setting.UI.Admin.NoticePagingNum)
if err != nil {
ctx.ServerError("Notices", err)
return
}
ctx.Data["Notices"] = notices
ctx.Data["Total"] = total
ctx.Data["Page"] = context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
ctx.HTML(http.StatusOK, tplNotices)
}
// DeleteNotices delete the specific notices
func DeleteNotices(ctx *context.Context) {
strs := ctx.QueryStrings("ids[]")
ids := make([]int64, 0, len(strs))
for i := range strs {
id, _ := strconv.ParseInt(strs[i], 10, 64)
if id > 0 {
ids = append(ids, id)
}
}
if err := models.DeleteNoticesByIDs(ids); err != nil {
ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error())
ctx.Status(500)
} else {
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
ctx.Status(200)
}
}
// EmptyNotices delete all the notices
func EmptyNotices(ctx *context.Context) {
if err := models.DeleteNotices(0, 0); err != nil {
ctx.ServerError("DeleteNotices", err)
return
}
log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.User.Name, 0)
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
ctx.Redirect(setting.AppSubURL + "/admin/notices")
}

34
routers/web/admin/orgs.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package admin
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/web/explore"
)
const (
tplOrgs base.TplName = "admin/org/list"
)
// Organizations show all the organizations
func Organizations(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.organizations")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminOrganizations"] = true
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
Type: models.UserTypeOrganization,
ListOptions: models.ListOptions{
PageSize: setting.UI.Admin.OrgPagingNum,
},
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
}, tplOrgs)
}

166
routers/web/admin/repos.go Normal file
View File

@@ -0,0 +1,166 @@
// 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 admin
import (
"net/http"
"net/url"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/explore"
repo_service "code.gitea.io/gitea/services/repository"
)
const (
tplRepos base.TplName = "admin/repo/list"
tplUnadoptedRepos base.TplName = "admin/repo/unadopted"
)
// Repos show all the repositories
func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminRepositories"] = true
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
PageSize: setting.UI.Admin.RepoPagingNum,
TplName: tplRepos,
})
}
// DeleteRepo delete one repository
func DeleteRepo(ctx *context.Context) {
repo, err := models.GetRepositoryByID(ctx.QueryInt64("id"))
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}
if ctx.Repo != nil && ctx.Repo.GitRepo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repo.ID {
ctx.Repo.GitRepo.Close()
}
if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
ctx.ServerError("DeleteRepository", err)
return
}
log.Trace("Repository deleted: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/repos?page=" + ctx.Query("page") + "&sort=" + ctx.Query("sort"),
})
}
// UnadoptedRepos lists the unadopted repositories
func UnadoptedRepos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminRepositories"] = true
opts := models.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.QueryInt("page"),
}
if opts.Page <= 0 {
opts.Page = 1
}
ctx.Data["CurrentPage"] = opts.Page
doSearch := ctx.QueryBool("search")
ctx.Data["search"] = doSearch
q := ctx.Query("q")
if !doSearch {
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "search", "search")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
return
}
ctx.Data["Keyword"] = q
repoNames, count, err := repository.ListUnadoptedRepositories(q, &opts)
if err != nil {
ctx.ServerError("ListUnadoptedRepositories", err)
}
ctx.Data["Dirs"] = repoNames
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "search", "search")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
}
// AdoptOrDeleteRepository adopts or deletes a repository
func AdoptOrDeleteRepository(ctx *context.Context) {
dir := ctx.Query("id")
action := ctx.Query("action")
page := ctx.QueryInt("page")
q := ctx.Query("q")
dirSplit := strings.SplitN(dir, "/", 2)
if len(dirSplit) != 2 {
ctx.Redirect(setting.AppSubURL + "/admin/repos")
return
}
ctxUser, err := models.GetUserByName(dirSplit[0])
if err != nil {
if models.IsErrUserNotExist(err) {
log.Debug("User does not exist: %s", dirSplit[0])
ctx.Redirect(setting.AppSubURL + "/admin/repos")
return
}
ctx.ServerError("GetUserByName", err)
return
}
repoName := dirSplit[1]
// check not a repo
has, err := models.IsRepositoryExist(ctxUser, repoName)
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
}
isDir, err := util.IsDir(models.RepoPath(ctxUser.Name, repoName))
if err != nil {
ctx.ServerError("IsDir", err)
return
}
if has || !isDir {
// Fallthrough to failure mode
} else if action == "adopt" {
if _, err := repository.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{
Name: dirSplit[1],
IsPrivate: true,
}); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
} else if action == "delete" {
if err := repository.DeleteUnadoptedRepository(ctx.User, ctxUser, dirSplit[1]); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
}
ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + strconv.Itoa(page))
}

371
routers/web/admin/users.go Normal file
View File

@@ -0,0 +1,371 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package admin
import (
"fmt"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
router_user_setting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
)
const (
tplUsers base.TplName = "admin/user/list"
tplUserNew base.TplName = "admin/user/new"
tplUserEdit base.TplName = "admin/user/edit"
)
// Users show all the users
func Users(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
Type: models.UserTypeIndividual,
ListOptions: models.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
},
SearchByEmail: true,
}, tplUsers)
}
// NewUser render adding a new user page
func NewUser(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
ctx.Data["login_type"] = "0-0"
sources, err := models.LoginSources()
if err != nil {
ctx.ServerError("LoginSources", err)
return
}
ctx.Data["Sources"] = sources
ctx.Data["CanSendEmail"] = setting.MailService != nil
ctx.HTML(http.StatusOK, tplUserNew)
}
// NewUserPost response for adding a new user
func NewUserPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminCreateUserForm)
ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
sources, err := models.LoginSources()
if err != nil {
ctx.ServerError("LoginSources", err)
return
}
ctx.Data["Sources"] = sources
ctx.Data["CanSendEmail"] = setting.MailService != nil
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplUserNew)
return
}
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
IsActive: true,
LoginType: models.LoginPlain,
}
if len(form.LoginType) > 0 {
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
lType, _ := strconv.ParseInt(fields[0], 10, 0)
u.LoginType = models.LoginType(lType)
u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64)
u.LoginName = form.LoginName
}
}
if u.LoginType == models.LoginNoType || u.LoginType == models.LoginPlain {
if len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
return
}
if !password.IsComplexEnough(form.Password) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.RenderWithErr(errMsg, tplUserNew, &form)
return
}
u.MustChangePassword = form.MustChangePassword
}
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
case models.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
case models.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
case models.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tplUserNew, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
default:
ctx.ServerError("CreateUser", err)
}
return
}
log.Trace("Account created by admin (%s): %s", ctx.User.Name, u.Name)
// Send email notification.
if form.SendNotify {
mailer.SendRegisterNotifyMail(u)
}
ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + fmt.Sprint(u.ID))
}
func prepareUserInfo(ctx *context.Context) *models.User {
u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
if err != nil {
ctx.ServerError("GetUserByID", err)
return nil
}
ctx.Data["User"] = u
if u.LoginSource > 0 {
ctx.Data["LoginSource"], err = models.GetLoginSourceByID(u.LoginSource)
if err != nil {
ctx.ServerError("GetLoginSourceByID", err)
return nil
}
} else {
ctx.Data["LoginSource"] = &models.LoginSource{}
}
sources, err := models.LoginSources()
if err != nil {
ctx.ServerError("LoginSources", err)
return nil
}
ctx.Data["Sources"] = sources
ctx.Data["TwoFactorEnabled"] = true
_, err = models.GetTwoFactorByUID(u.ID)
if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
return nil
}
ctx.Data["TwoFactorEnabled"] = false
}
return u
}
// EditUser show editting user page
func EditUser(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
prepareUserInfo(ctx)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplUserEdit)
}
// EditUserPost response for editting user
func EditUserPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminEditUserForm)
ctx.Data["Title"] = ctx.Tr("admin.users.edit_account")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminUsers"] = true
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
u := prepareUserInfo(ctx)
if ctx.Written() {
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplUserEdit)
return
}
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
loginType, _ := strconv.ParseInt(fields[0], 10, 0)
loginSource, _ := strconv.ParseInt(fields[1], 10, 64)
if u.LoginSource != loginSource {
u.LoginSource = loginSource
u.LoginType = models.LoginType(loginType)
}
}
if len(form.Password) > 0 && (u.IsLocal() || u.IsOAuth2()) {
var err error
if len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
return
}
if !password.IsComplexEnough(form.Password) {
ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form)
return
}
pwned, err := password.IsPwned(ctx, form.Password)
if pwned {
ctx.Data["Err_Password"] = true
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.RenderWithErr(errMsg, tplUserNew, &form)
return
}
if u.Salt, err = models.GetUserSalt(); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err = u.SetPassword(form.Password); err != nil {
ctx.ServerError("SetPassword", err)
return
}
}
if len(form.UserName) != 0 && u.Name != form.UserName {
if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil {
ctx.Redirect(setting.AppSubURL + "/admin/users")
return
}
u.Name = form.UserName
u.LowerName = strings.ToLower(form.UserName)
}
if form.Reset2FA {
tf, err := models.GetTwoFactorByUID(u.ID)
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("GetTwoFactorByUID", err)
return
}
if err = models.DeleteTwoFactorByID(tf.ID, u.ID); err != nil {
ctx.ServerError("DeleteTwoFactorByID", err)
return
}
}
u.LoginName = form.LoginName
u.FullName = form.FullName
u.Email = form.Email
u.Website = form.Website
u.Location = form.Location
u.MaxRepoCreation = form.MaxRepoCreation
u.IsActive = form.Active
u.IsAdmin = form.Admin
u.IsRestricted = form.Restricted
u.AllowGitHook = form.AllowGitHook
u.AllowImportLocal = form.AllowImportLocal
u.AllowCreateOrganization = form.AllowCreateOrganization
// skip self Prohibit Login
if ctx.User.ID == u.ID {
u.ProhibitLogin = false
} else {
u.ProhibitLogin = form.ProhibitLogin
}
if err := models.UpdateUser(u); err != nil {
if models.IsErrEmailAlreadyUsed(err) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
} else if models.IsErrEmailInvalid(err) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
} else {
ctx.ServerError("UpdateUser", err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
}
// DeleteUser response for deleting a user
func DeleteUser(ctx *context.Context) {
u, err := models.GetUserByID(ctx.ParamsInt64(":userid"))
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
if err = models.DeleteUser(u); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
})
case models.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
})
default:
ctx.ServerError("DeleteUser", err)
}
return
}
log.Trace("Account deleted by admin (%s): %s", ctx.User.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/admin/users",
})
}

View File

@@ -0,0 +1,123 @@
// Copyright 2017 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 admin
import (
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
)
func TestNewUserPost_MustChangePassword(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "admin/users/new")
u := models.AssertExistsAndLoadBean(t, &models.User{
IsAdmin: true,
ID: 2,
}).(*models.User)
ctx.User = u
username := "gitea"
email := "gitea@gitea.io"
form := forms.AdminCreateUserForm{
LoginType: "local",
LoginName: "local",
UserName: username,
Email: email,
Password: "abc123ABC!=$",
SendNotify: false,
MustChangePassword: true,
}
web.SetForm(ctx, &form)
NewUserPost(ctx)
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
u, err := models.GetUserByName(username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
assert.Equal(t, email, u.Email)
assert.True(t, u.MustChangePassword)
}
func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "admin/users/new")
u := models.AssertExistsAndLoadBean(t, &models.User{
IsAdmin: true,
ID: 2,
}).(*models.User)
ctx.User = u
username := "gitea"
email := "gitea@gitea.io"
form := forms.AdminCreateUserForm{
LoginType: "local",
LoginName: "local",
UserName: username,
Email: email,
Password: "abc123ABC!=$",
SendNotify: false,
MustChangePassword: false,
}
web.SetForm(ctx, &form)
NewUserPost(ctx)
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
u, err := models.GetUserByName(username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
assert.Equal(t, email, u.Email)
assert.False(t, u.MustChangePassword)
}
func TestNewUserPost_InvalidEmail(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "admin/users/new")
u := models.AssertExistsAndLoadBean(t, &models.User{
IsAdmin: true,
ID: 2,
}).(*models.User)
ctx.User = u
username := "gitea"
email := "gitea@gitea.io\r\n"
form := forms.AdminCreateUserForm{
LoginType: "local",
LoginName: "local",
UserName: username,
Email: email,
Password: "abc123ABC!=$",
SendNotify: false,
MustChangePassword: false,
}
web.SetForm(ctx, &form)
NewUserPost(ctx)
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}

189
routers/web/base.go Normal file
View File

@@ -0,0 +1,189 @@
// Copyright 2020 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 web
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/session"
)
func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
if storageSetting.ServeDirect {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" && req.Method != "HEAD" {
next.ServeHTTP(w, req)
return
}
if !strings.HasPrefix(req.URL.RequestURI(), "/"+prefix) {
next.ServeHTTP(w, req)
return
}
rPath := strings.TrimPrefix(req.URL.RequestURI(), "/"+prefix)
u, err := objStore.URL(rPath, path.Base(rPath))
if err != nil {
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
log.Warn("Unable to find %s %s", prefix, rPath)
http.Error(w, "file not found", 404)
return
}
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), 500)
return
}
http.Redirect(
w,
req,
u.String(),
301,
)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" && req.Method != "HEAD" {
next.ServeHTTP(w, req)
return
}
prefix := strings.Trim(prefix, "/")
if !strings.HasPrefix(req.URL.EscapedPath(), "/"+prefix+"/") {
next.ServeHTTP(w, req)
return
}
rPath := strings.TrimPrefix(req.URL.EscapedPath(), "/"+prefix+"/")
rPath = strings.TrimPrefix(rPath, "/")
if rPath == "" {
http.Error(w, "file not found", 404)
return
}
rPath = path.Clean("/" + filepath.ToSlash(rPath))
rPath = rPath[1:]
fi, err := objStore.Stat(rPath)
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
return
}
//If we have matched and access to release or issue
fr, err := objStore.Open(rPath)
if err != nil {
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
log.Warn("Unable to find %s %s", prefix, rPath)
http.Error(w, "file not found", 404)
return
}
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), 500)
return
}
defer fr.Close()
_, err = io.Copy(w, fr)
if err != nil {
log.Error("Error whilst rendering %s %s. Error: %v", prefix, rPath, err)
http.Error(w, fmt.Sprintf("Error whilst rendering %s %s", prefix, rPath), 500)
return
}
})
}
}
type dataStore map[string]interface{}
func (d *dataStore) GetData() map[string]interface{} {
return *d
}
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
// This error will be created with the gitea 500 page.
func Recovery() func(next http.Handler) http.Handler {
var rnd = templates.HTMLRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
log.Error("%v", combinedErr)
sessionStore := session.GetSession(req)
if sessionStore == nil {
if setting.IsProd() {
http.Error(w, http.StatusText(500), 500)
} else {
http.Error(w, combinedErr, 500)
}
return
}
var lc = middleware.Locale(w, req)
var store = dataStore{
"Language": lc.Language(),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"i18n": lc,
}
var user *models.User
if apiContext := context.GetAPIContext(req); apiContext != nil {
user = apiContext.User
}
if user == nil {
if ctx := context.GetContext(req); ctx != nil {
user = ctx.User
}
}
if user == nil {
// Get user from session if logged in - do not attempt to sign-in
user = sso.SessionUser(sessionStore)
}
if user != nil {
store["IsSigned"] = true
store["SignedUser"] = user
store["SignedUserID"] = user.ID
store["SignedUserName"] = user.Name
store["IsAdmin"] = user.IsAdmin
} else {
store["SignedUserID"] = int64(0)
store["SignedUserName"] = ""
}
w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
if !setting.IsProd() {
store["ErrorMsg"] = combinedErr
}
err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store))
if err != nil {
log.Error("%v", err)
}
}
}()
next.ServeHTTP(w, req)
})
}
}

View File

@@ -0,0 +1,29 @@
// 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 dev
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
)
// TemplatePreview render for previewing the indicated template
func TemplatePreview(ctx *context.Context) {
ctx.Data["User"] = models.User{Name: "Unknown"}
ctx.Data["AppName"] = setting.AppName
ctx.Data["AppVer"] = setting.AppVer
ctx.Data["AppUrl"] = setting.AppURL
ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374"
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
ctx.Data["CurDbValue"] = ""
ctx.HTML(http.StatusOK, base.TplName(ctx.Params("*")))
}

View File

@@ -0,0 +1,156 @@
// Copyright 2020 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 events
import (
"net/http"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/user"
jsoniter "github.com/json-iterator/go"
)
// Events listens for events
func Events(ctx *context.Context) {
// FIXME: Need to check if resp is actually a http.Flusher! - how though?
// Set the headers related to event streaming.
ctx.Resp.Header().Set("Content-Type", "text/event-stream")
ctx.Resp.Header().Set("Cache-Control", "no-cache")
ctx.Resp.Header().Set("Connection", "keep-alive")
ctx.Resp.Header().Set("X-Accel-Buffering", "no")
ctx.Resp.WriteHeader(http.StatusOK)
if !ctx.IsSigned {
// Return unauthorized status event
event := &eventsource.Event{
Name: "close",
Data: "unauthorized",
}
_, _ = event.WriteTo(ctx)
ctx.Resp.Flush()
return
}
// Listen to connection close and un-register messageChan
notify := ctx.Done()
ctx.Resp.Flush()
shutdownCtx := graceful.GetManager().ShutdownContext()
uid := ctx.User.ID
messageChan := eventsource.GetManager().Register(uid)
unregister := func() {
eventsource.GetManager().Unregister(uid, messageChan)
// ensure the messageChan is closed
for {
_, ok := <-messageChan
if !ok {
break
}
}
}
if _, err := ctx.Resp.Write([]byte("\n")); err != nil {
log.Error("Unable to write to EventStream: %v", err)
unregister()
return
}
timer := time.NewTicker(30 * time.Second)
stopwatchTimer := time.NewTicker(setting.UI.Notification.MinTimeout)
loop:
for {
select {
case <-timer.C:
event := &eventsource.Event{
Name: "ping",
}
_, err := event.WriteTo(ctx.Resp)
if err != nil {
log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
go unregister()
break loop
}
ctx.Resp.Flush()
case <-notify:
go unregister()
break loop
case <-shutdownCtx.Done():
go unregister()
break loop
case <-stopwatchTimer.C:
sws, err := models.GetUserStopwatches(ctx.User.ID, models.ListOptions{})
if err != nil {
log.Error("Unable to GetUserStopwatches: %v", err)
continue
}
apiSWs, err := convert.ToStopWatches(sws)
if err != nil {
log.Error("Unable to APIFormat stopwatches: %v", err)
continue
}
json := jsoniter.ConfigCompatibleWithStandardLibrary
dataBs, err := json.Marshal(apiSWs)
if err != nil {
log.Error("Unable to marshal stopwatches: %v", err)
continue
}
_, err = (&eventsource.Event{
Name: "stopwatches",
Data: string(dataBs),
}).WriteTo(ctx.Resp)
if err != nil {
log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
go unregister()
break loop
}
ctx.Resp.Flush()
case event, ok := <-messageChan:
if !ok {
break loop
}
// Handle logout
if event.Name == "logout" {
if ctx.Session.ID() == event.Data {
_, _ = (&eventsource.Event{
Name: "logout",
Data: "here",
}).WriteTo(ctx.Resp)
ctx.Resp.Flush()
go unregister()
user.HandleSignOut(ctx)
break loop
}
// Replace the event - we don't want to expose the session ID to the user
event = &eventsource.Event{
Name: "logout",
Data: "elsewhere",
}
}
_, err := event.WriteTo(ctx.Resp)
if err != nil {
log.Error("Unable to write to EventStream for user %s: %v", ctx.User.Name, err)
go unregister()
break loop
}
ctx.Resp.Flush()
}
}
timer.Stop()
}

139
routers/web/explore/code.go Normal file
View File

@@ -0,0 +1,139 @@
// Copyright 2021 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 explore
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
)
const (
// tplExploreCode explore code page template
tplExploreCode base.TplName = "explore/code"
)
// Code render explore code page
func Code(ctx *context.Context) {
if !setting.Indexer.RepoIndexerEnabled {
ctx.Redirect(setting.AppSubURL+"/explore", 302)
return
}
ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
ctx.Data["PageIsExploreCode"] = true
language := strings.TrimSpace(ctx.Query("l"))
keyword := strings.TrimSpace(ctx.Query("q"))
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
queryType := strings.TrimSpace(ctx.Query("t"))
isMatch := queryType == "match"
var (
repoIDs []int64
err error
isAdmin bool
)
if ctx.User != nil {
isAdmin = ctx.User.IsAdmin
}
// guest user or non-admin user
if ctx.User == nil || !isAdmin {
repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
}
var (
total int
searchResults []*code_indexer.Result
searchResultLanguages []*code_indexer.SearchResultLanguages
)
// if non-admin login user, we need check UnitTypeCode at first
if ctx.User != nil && len(repoIDs) > 0 {
repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps))
repoIDs = make([]int64, 0, len(repoMaps))
for id, repo := range repoMaps {
if repo.CheckUnitUser(ctx.User, models.UnitTypeCode) {
rightRepoMap[id] = repo
repoIDs = append(repoIDs, id)
}
}
ctx.Data["RepoMaps"] = rightRepoMap
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
// if non-login user or isAdmin, no need to check UnitTypeCode
} else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
var loadRepoIDs = make([]int64, 0, len(searchResults))
for _, result := range searchResults {
var find bool
for _, id := range loadRepoIDs {
if id == result.RepoID {
find = true
break
}
}
if !find {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
ctx.Data["RepoMaps"] = repoMaps
}
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
ctx.Data["queryType"] = queryType
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages
ctx.Data["RequireHighlightJS"] = true
ctx.Data["PageIsViewCode"] = true
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplExploreCode)
}

View File

@@ -0,0 +1,39 @@
// Copyright 2021 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 explore
import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
)
const (
// tplExploreOrganizations explore organizations page template
tplExploreOrganizations base.TplName = "explore/organizations"
)
// Organizations render explore organizations page
func Organizations(ctx *context.Context) {
ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
ctx.Data["PageIsExploreOrganizations"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
if ctx.User != nil {
visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
}
RenderUserSearch(ctx, &models.SearchUserOptions{
Actor: ctx.User,
Type: models.UserTypeOrganization,
ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
Visible: visibleTypes,
}, tplExploreOrganizations)
}

131
routers/web/explore/repo.go Normal file
View File

@@ -0,0 +1,131 @@
// Copyright 2021 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 explore
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
// tplExploreRepos explore repositories page template
tplExploreRepos base.TplName = "explore/repos"
)
// RepoSearchOptions when calling search repositories
type RepoSearchOptions struct {
OwnerID int64
Private bool
Restricted bool
PageSize int
TplName base.TplName
}
// RenderRepoSearch render repositories search page
func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
var (
repos []*models.Repository
count int64
err error
orderBy models.SearchOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "newest":
orderBy = models.SearchOrderByNewest
case "oldest":
orderBy = models.SearchOrderByOldest
case "recentupdate":
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = models.SearchOrderByAlphabetically
case "reversesize":
orderBy = models.SearchOrderBySizeReverse
case "size":
orderBy = models.SearchOrderBySize
case "moststars":
orderBy = models.SearchOrderByStarsReverse
case "feweststars":
orderBy = models.SearchOrderByStars
case "mostforks":
orderBy = models.SearchOrderByForksReverse
case "fewestforks":
orderBy = models.SearchOrderByForks
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = models.SearchOrderByRecentUpdated
}
keyword := strings.Trim(ctx.Query("q"), " ")
topicOnly := ctx.QueryBool("topic")
ctx.Data["TopicOnly"] = topicOnly
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
ListOptions: models.ListOptions{
Page: page,
PageSize: opts.PageSize,
},
Actor: ctx.User,
OrderBy: orderBy,
Private: opts.Private,
Keyword: keyword,
OwnerID: opts.OwnerID,
AllPublic: true,
AllLimited: true,
TopicOnly: topicOnly,
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
ctx.Data["Keyword"] = keyword
ctx.Data["Total"] = count
ctx.Data["Repos"] = repos
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
pager := context.NewPagination(int(count), opts.PageSize, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "topic", "TopicOnly")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, opts.TplName)
}
// Repos render explore repositories page
func Repos(ctx *context.Context) {
ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
ctx.Data["PageIsExploreRepositories"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
var ownerID int64
if ctx.User != nil && !ctx.User.IsAdmin {
ownerID = ctx.User.ID
}
RenderRepoSearch(ctx, &RepoSearchOptions{
PageSize: setting.UI.ExplorePagingNum,
OwnerID: ownerID,
Private: ctx.User != nil,
TplName: tplExploreRepos,
})
}

107
routers/web/explore/user.go Normal file
View File

@@ -0,0 +1,107 @@
// Copyright 2021 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 explore
import (
"bytes"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
const (
// tplExploreUsers explore users page template
tplExploreUsers base.TplName = "explore/users"
)
var (
nullByte = []byte{0x00}
)
func isKeywordValid(keyword string) bool {
return !bytes.Contains([]byte(keyword), nullByte)
}
// RenderUserSearch render user search page
func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplName base.TplName) {
opts.Page = ctx.QueryInt("page")
if opts.Page <= 1 {
opts.Page = 1
}
var (
users []*models.User
count int64
err error
orderBy models.SearchOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "newest":
orderBy = models.SearchOrderByIDReverse
case "oldest":
orderBy = models.SearchOrderByID
case "recentupdate":
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = models.SearchOrderByAlphabetically
default:
ctx.Data["SortType"] = "alphabetically"
orderBy = models.SearchOrderByAlphabetically
}
opts.Keyword = strings.Trim(ctx.Query("q"), " ")
opts.OrderBy = orderBy
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
users, count, err = models.SearchUsers(opts)
if err != nil {
ctx.ServerError("SearchUsers", err)
return
}
}
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Users"] = users
ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplName)
}
// Users render explore users page
func Users(ctx *context.Context) {
if setting.Service.Explore.DisableUsersPage {
ctx.Redirect(setting.AppSubURL + "/explore/repos")
return
}
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
ctx.Data["PageIsExploreUsers"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
RenderUserSearch(ctx, &models.SearchUserOptions{
Actor: ctx.User,
Type: models.UserTypeIndividual,
ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
IsActive: util.OptionalBoolTrue,
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
}, tplExploreUsers)
}

86
routers/web/goget.go Normal file
View File

@@ -0,0 +1,86 @@
// Copyright 2021 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 web
import (
"net/http"
"net/url"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/unknwon/com"
)
func goGet(ctx *context.Context) {
if ctx.Req.Method != "GET" || ctx.Query("go-get") != "1" || len(ctx.Req.URL.Query()) > 1 {
return
}
parts := strings.SplitN(ctx.Req.URL.EscapedPath(), "/", 4)
if len(parts) < 3 {
return
}
ownerName := parts[1]
repoName := parts[2]
// Quick responses appropriate go-get meta with status 200
// regardless of if user have access to the repository,
// or the repository does not exist at all.
// This is particular a workaround for "go get" command which does not respect
// .netrc file.
trimmedRepoName := strings.TrimSuffix(repoName, ".git")
if ownerName == "" || trimmedRepoName == "" {
_, _ = ctx.Write([]byte(`<!doctype html>
<html>
<body>
invalid import path
</body>
</html>
`))
ctx.Status(400)
return
}
branchName := setting.Repository.DefaultBranch
repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName)
if err == nil && len(repo.DefaultBranch) > 0 {
branchName = repo.DefaultBranch
}
prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName))
appURL, _ := url.Parse(setting.AppURL)
insecure := ""
if appURL.Scheme == string(setting.HTTP) {
insecure = "--insecure "
}
ctx.Header().Set("Content-Type", "text/html")
ctx.Status(http.StatusOK)
_, _ = ctx.Write([]byte(com.Expand(`<!doctype html>
<html>
<head>
<meta name="go-import" content="{GoGetImport} git {CloneLink}">
<meta name="go-source" content="{GoGetImport} _ {GoDocDirectory} {GoDocFile}">
</head>
<body>
go get {Insecure}{GoGetImport}
</body>
</html>
`, map[string]string{
"GoGetImport": context.ComposeGoGetImport(ownerName, trimmedRepoName),
"CloneLink": models.ComposeHTTPSCloneURL(ownerName, repoName),
"GoDocDirectory": prefix + "{/dir}",
"GoDocFile": prefix + "{/dir}/{file}#L{line}",
"Insecure": insecure,
})))
}

65
routers/web/home.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 web
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/web/user"
)
const (
// tplHome home page template
tplHome base.TplName = "home"
)
// Home render home page
func Home(ctx *context.Context) {
if ctx.IsSigned {
if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, user.TplActivate)
} else if !ctx.User.IsActive || ctx.User.ProhibitLogin {
log.Info("Failed authentication attempt for %s from %s", ctx.User.Name, ctx.RemoteAddr())
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
} else if ctx.User.MustChangePassword {
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
} else {
user.Dashboard(ctx)
}
return
// Check non-logged users landing page.
} else if setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
}
// Check auto-login.
uname := ctx.GetCookie(setting.CookieUserName)
if len(uname) != 0 {
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
ctx.Data["PageIsHome"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.HTML(http.StatusOK, tplHome)
}
// NotFound render 404 page
func NotFound(ctx *context.Context) {
ctx.Data["Title"] = "Page Not Found"
ctx.NotFound("home.NotFound", nil)
}

34
routers/web/metrics.go Normal file
View File

@@ -0,0 +1,34 @@
// 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 web
import (
"crypto/subtle"
"net/http"
"code.gitea.io/gitea/modules/setting"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Metrics validate auth token and render prometheus metrics
func Metrics(resp http.ResponseWriter, req *http.Request) {
if setting.Metrics.Token == "" {
promhttp.Handler().ServeHTTP(resp, req)
return
}
header := req.Header.Get("Authorization")
if header == "" {
http.Error(resp, "", 401)
return
}
got := []byte(header)
want := []byte("Bearer " + setting.Metrics.Token)
if subtle.ConstantTimeCompare(got, want) != 1 {
http.Error(resp, "", 401)
return
}
promhttp.Handler().ServeHTTP(resp, req)
}

151
routers/web/org/home.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2019 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 org
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
)
const (
tplOrgHome base.TplName = "org/home"
)
// Home show organization home page
func Home(ctx *context.Context) {
ctx.SetParams(":org", ctx.Params(":username"))
context.HandleOrgAssignment(ctx)
if ctx.Written() {
return
}
org := ctx.Org.Organization
if !models.HasOrgVisible(org, ctx.User) {
ctx.NotFound("HasOrgVisible", nil)
return
}
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Title"] = org.DisplayName()
if len(org.Description) != 0 {
desc, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: map[string]string{"mode": "document"},
}, org.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["RenderedDescription"] = desc
}
var orderBy models.SearchOrderBy
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "newest":
orderBy = models.SearchOrderByNewest
case "oldest":
orderBy = models.SearchOrderByOldest
case "recentupdate":
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = models.SearchOrderByAlphabetically
case "moststars":
orderBy = models.SearchOrderByStarsReverse
case "feweststars":
orderBy = models.SearchOrderByStars
case "mostforks":
orderBy = models.SearchOrderByForksReverse
case "fewestforks":
orderBy = models.SearchOrderByForks
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = models.SearchOrderByRecentUpdated
}
keyword := strings.Trim(ctx.Query("q"), " ")
ctx.Data["Keyword"] = keyword
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
var (
repos []*models.Repository
count int64
err error
)
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
ListOptions: models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
},
Keyword: keyword,
OwnerID: org.ID,
OrderBy: orderBy,
Private: ctx.IsSigned,
Actor: ctx.User,
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
var opts = models.FindOrgMembersOpts{
OrgID: org.ID,
PublicOnly: true,
ListOptions: models.ListOptions{Page: 1, PageSize: 25},
}
if ctx.User != nil {
isMember, err := org.IsOrgMember(ctx.User.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrgMember")
return
}
opts.PublicOnly = !isMember && !ctx.User.IsAdmin
}
members, _, err := models.FindOrgMembers(&opts)
if err != nil {
ctx.ServerError("FindOrgMembers", err)
return
}
membersCount, err := models.CountOrgMembers(opts)
if err != nil {
ctx.ServerError("CountOrgMembers", err)
return
}
ctx.Data["Owner"] = org
ctx.Data["Repos"] = repos
ctx.Data["Total"] = count
ctx.Data["MembersTotal"] = membersCount
ctx.Data["Members"] = members
ctx.Data["Teams"] = org.Teams
ctx.Data["DisabledMirrors"] = setting.Repository.DisableMirrors
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplOrgHome)
}

128
routers/web/org/members.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package org
import (
"net/http"
"code.gitea.io/gitea/models"
"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 (
// tplMembers template for organization members page
tplMembers base.TplName = "org/member/members"
)
// Members render organization users page
func Members(ctx *context.Context) {
org := ctx.Org.Organization
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgMembers"] = true
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
var opts = models.FindOrgMembersOpts{
OrgID: org.ID,
PublicOnly: true,
}
if ctx.User != nil {
isMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsOrgMember")
return
}
opts.PublicOnly = !isMember && !ctx.User.IsAdmin
}
total, err := models.CountOrgMembers(opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountOrgMembers")
return
}
pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5)
opts.ListOptions.Page = page
opts.ListOptions.PageSize = setting.UI.MembersPagingNum
members, membersIsPublic, err := models.FindOrgMembers(&opts)
if err != nil {
ctx.ServerError("GetMembers", err)
return
}
ctx.Data["Page"] = pager
ctx.Data["Members"] = members
ctx.Data["MembersIsPublicMember"] = membersIsPublic
ctx.Data["MembersIsUserOrgOwner"] = members.IsUserOrgOwner(org.ID)
ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus()
ctx.HTML(http.StatusOK, tplMembers)
}
// MembersAction response for operation to a member of organization
func MembersAction(ctx *context.Context) {
uid := ctx.QueryInt64("uid")
if uid == 0 {
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
org := ctx.Org.Organization
var err error
switch ctx.Params(":action") {
case "private":
if ctx.User.ID != uid && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
err = models.ChangeOrgUserStatus(org.ID, uid, false)
case "public":
if ctx.User.ID != uid && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
err = models.ChangeOrgUserStatus(org.ID, uid, true)
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
err = org.RemoveMember(uid)
if models.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
case "leave":
err = org.RemoveMember(ctx.User.ID)
if models.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
}
if err != nil {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": false,
"err": err.Error(),
})
return
}
if ctx.Params(":action") != "leave" {
ctx.Redirect(ctx.Org.OrgLink + "/members")
} else {
ctx.Redirect(setting.AppSubURL + "/")
}
}

79
routers/web/org/org.go Normal file
View File

@@ -0,0 +1,79 @@
// 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 org
import (
"errors"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
const (
// tplCreateOrg template path for create organization
tplCreateOrg base.TplName = "org/create"
)
// Create render the page for create organization
func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.User.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
return
}
ctx.HTML(http.StatusOK, tplCreateOrg)
}
// CreatePost response for create organization
func CreatePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.CreateOrgForm)
ctx.Data["Title"] = ctx.Tr("new_org")
if !ctx.User.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
return
}
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplCreateOrg)
return
}
org := &models.User{
Name: form.OrgName,
IsActive: true,
Type: models.UserTypeOrganization,
Visibility: form.Visibility,
RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
}
if err := models.CreateOrganization(org, ctx.User); err != nil {
ctx.Data["Err_OrgName"] = true
switch {
case models.IsErrUserAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
case models.IsErrNameReserved(err):
ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(models.ErrNameReserved).Name), tplCreateOrg, &form)
case models.IsErrNamePatternNotAllowed(err):
ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
case models.IsErrUserNotAllowedCreateOrg(err):
ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
default:
ctx.ServerError("CreateOrganization", err)
}
return
}
log.Trace("Organization created: %s", org.Name)
ctx.Redirect(org.DashboardLink())
}

View File

@@ -0,0 +1,112 @@
// Copyright 2020 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 org
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
// RetrieveLabels find all the labels of an organization
func RetrieveLabels(ctx *context.Context) {
labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
}
for _, l := range labels {
l.CalOpenIssues()
}
ctx.Data["Labels"] = labels
ctx.Data["NumLabels"] = len(labels)
ctx.Data["SortType"] = ctx.Query("sort")
}
// NewLabel create new label for organization
func NewLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsLabels"] = true
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
return
}
l := &models.Label{
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Description: form.Description,
Color: form.Color,
}
if err := models.NewLabel(l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
if err != nil {
switch {
case models.IsErrOrgLabelNotExist(err):
ctx.Error(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
}
return
}
l.Name = form.Title
l.Description = form.Description
l.Color = form.Color
if err := models.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}
// DeleteLabel delete a label
func DeleteLabel(ctx *context.Context) {
if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteLabel: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/settings/labels",
})
}
// InitializeLabels init labels for an organization
func InitializeLabels(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
if ctx.HasError() {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
return
}
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
if models.IsErrIssueLabelTemplateLoad(err) {
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
return
}
ctx.ServerError("InitializeLabels", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
}

219
routers/web/org/setting.go Normal file
View File

@@ -0,0 +1,219 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 org
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
userSetting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/services/forms"
)
const (
// tplSettingsOptions template path for render settings
tplSettingsOptions base.TplName = "org/settings/options"
// tplSettingsDelete template path for render delete repository
tplSettingsDelete base.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks base.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
tplSettingsLabels base.TplName = "org/settings/labels"
)
// Settings render the main settings page
func Settings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.HTML(http.StatusOK, tplSettingsOptions)
}
// SettingsPost response for settings change submited
func SettingsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateOrgSettingForm)
ctx.Data["Title"] = ctx.Tr("org.settings")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsOptions)
return
}
org := ctx.Org.Organization
nameChanged := org.Name != form.Name
// Check if organization name has been changed.
if org.LowerName != strings.ToLower(form.Name) {
isExist, err := models.IsUserExist(org.ID, form.Name)
if err != nil {
ctx.ServerError("IsUserExist", err)
return
} else if isExist {
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
return
} else if err = models.ChangeUserName(org, form.Name); err != nil {
if err == models.ErrUserNameIllegal {
ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.illegal_username"), tplSettingsOptions, &form)
} else {
ctx.ServerError("ChangeUserName", err)
}
return
}
// reset ctx.org.OrgLink with new name
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + form.Name
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
nameChanged = false
}
// In case it's just a case change.
org.Name = form.Name
org.LowerName = strings.ToLower(form.Name)
if ctx.User.IsAdmin {
org.MaxRepoCreation = form.MaxRepoCreation
}
org.FullName = form.FullName
org.Description = form.Description
org.Website = form.Website
org.Location = form.Location
org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess
visibilityChanged := form.Visibility != org.Visibility
org.Visibility = form.Visibility
if err := models.UpdateUser(org); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
// update forks visibility
if visibilityChanged {
if err := org.GetRepositories(models.ListOptions{Page: 1, PageSize: org.NumRepos}); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
for _, repo := range org.Repos {
repo.OwnerName = org.Name
if err := models.UpdateRepository(repo, true); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
}
} else if nameChanged {
if err := models.UpdateRepositoryOwnerNames(org.ID, org.Name); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
}
log.Trace("Organization setting updated: %s", org.Name)
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
ctx.Redirect(ctx.Org.OrgLink + "/settings")
}
// SettingsAvatar response for change avatar on settings page
func SettingsAvatar(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AvatarForm)
form.Source = forms.AvatarLocal
if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil {
ctx.Flash.Error(err.Error())
} else {
ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success"))
}
ctx.Redirect(ctx.Org.OrgLink + "/settings")
}
// SettingsDeleteAvatar response for delete avatar on setings page
func SettingsDeleteAvatar(ctx *context.Context) {
if err := ctx.Org.Organization.DeleteAvatar(); err != nil {
ctx.Flash.Error(err.Error())
}
ctx.Redirect(ctx.Org.OrgLink + "/settings")
}
// SettingsDelete response for deleting an organization
func SettingsDelete(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings")
ctx.Data["PageIsSettingsDelete"] = true
org := ctx.Org.Organization
if ctx.Req.Method == "POST" {
if org.Name != ctx.Query("org_name") {
ctx.Data["Err_OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
return
}
if err := models.DeleteOrganization(org); err != nil {
if models.IsErrUserOwnRepos(err) {
ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
} else {
ctx.ServerError("DeleteOrganization", err)
}
} else {
log.Trace("Organization deleted: %s", org.Name)
ctx.Redirect(setting.AppSubURL + "/")
}
return
}
ctx.HTML(http.StatusOK, tplSettingsDelete)
}
// Webhooks render webhook list page
func Webhooks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings")
ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks"
ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
ws, err := models.GetWebhooksByOrgID(ctx.Org.Organization.ID, models.ListOptions{})
if err != nil {
ctx.ServerError("GetWebhooksByOrgId", err)
return
}
ctx.Data["Webhooks"] = ws
ctx.HTML(http.StatusOK, tplSettingsHooks)
}
// DeleteWebhook response for delete webhook
func DeleteWebhook(ctx *context.Context) {
if err := models.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/settings/hooks",
})
}
// Labels render organization labels page
func Labels(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsOrgSettingsLabels"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["LabelTemplates"] = models.LabelTemplates
ctx.HTML(http.StatusOK, tplSettingsLabels)
}

357
routers/web/org/teams.go Normal file
View File

@@ -0,0 +1,357 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 org
import (
"net/http"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
)
const (
// tplTeams template path for teams list page
tplTeams base.TplName = "org/team/teams"
// tplTeamNew template path for create new team page
tplTeamNew base.TplName = "org/team/new"
// tplTeamMembers template path for showing team members page
tplTeamMembers base.TplName = "org/team/members"
// tplTeamRepositories template path for showing team repositories page
tplTeamRepositories base.TplName = "org/team/repositories"
)
// Teams render teams list page
func Teams(ctx *context.Context) {
org := ctx.Org.Organization
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgTeams"] = true
for _, t := range org.Teams {
if err := t.GetMembers(&models.SearchMembersOptions{}); err != nil {
ctx.ServerError("GetMembers", err)
return
}
}
ctx.Data["Teams"] = org.Teams
ctx.HTML(http.StatusOK, tplTeams)
}
// TeamsAction response for join, leave, remove, add operations to team
func TeamsAction(ctx *context.Context) {
uid := ctx.QueryInt64("uid")
if uid == 0 {
ctx.Redirect(ctx.Org.OrgLink + "/teams")
return
}
page := ctx.Query("page")
var err error
switch ctx.Params(":action") {
case "join":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
err = ctx.Org.Team.AddMember(ctx.User.ID)
case "leave":
err = ctx.Org.Team.RemoveMember(ctx.User.ID)
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
err = ctx.Org.Team.RemoveMember(uid)
page = "team"
case "add":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("uname")))
var u *models.User
u, err = models.GetUserByName(uname)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
} else {
ctx.ServerError(" GetUserByName", err)
}
return
}
if u.IsOrganization() {
ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team"))
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
return
}
if ctx.Org.Team.IsMember(u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
err = ctx.Org.Team.AddMember(u.ID)
}
page = "team"
}
if err != nil {
if models.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
} else {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": false,
"err": err.Error(),
})
return
}
}
switch page {
case "team":
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName)
case "home":
ctx.Redirect(ctx.Org.Organization.HomeLink())
default:
ctx.Redirect(ctx.Org.OrgLink + "/teams")
}
}
// TeamsRepoAction operate team's repository
func TeamsRepoAction(ctx *context.Context) {
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
var err error
action := ctx.Params(":action")
switch action {
case "add":
repoName := path.Base(ctx.Query("repo_name"))
var repo *models.Repository
repo, err = models.GetRepositoryByName(ctx.Org.Organization.ID, repoName)
if err != nil {
if models.IsErrRepoNotExist(err) {
ctx.Flash.Error(ctx.Tr("org.teams.add_nonexistent_repo"))
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
return
}
ctx.ServerError("GetRepositoryByName", err)
return
}
err = ctx.Org.Team.AddRepository(repo)
case "remove":
err = ctx.Org.Team.RemoveRepository(ctx.QueryInt64("repoid"))
case "addall":
err = ctx.Org.Team.AddAllRepositories()
case "removeall":
err = ctx.Org.Team.RemoveAllRepositories()
}
if err != nil {
log.Error("Action(%s): '%s' %v", ctx.Params(":action"), ctx.Org.Team.Name, err)
ctx.ServerError("TeamsRepoAction", err)
return
}
if action == "addall" || action == "removeall" {
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories",
})
return
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + ctx.Org.Team.LowerName + "/repositories")
}
// NewTeam render create new team page
func NewTeam(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamsNew"] = true
ctx.Data["Team"] = &models.Team{}
ctx.Data["Units"] = models.Units
ctx.HTML(http.StatusOK, tplTeamNew)
}
// NewTeamPost response for create new team
func NewTeamPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateTeamForm)
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamsNew"] = true
ctx.Data["Units"] = models.Units
var includesAllRepositories = form.RepoAccess == "all"
t := &models.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.TeamName,
Description: form.Description,
Authorize: models.ParseAccessMode(form.Permission),
IncludesAllRepositories: includesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
}
if t.Authorize < models.AccessModeOwner {
var units = make([]*models.TeamUnit, 0, len(form.Units))
for _, tp := range form.Units {
units = append(units, &models.TeamUnit{
OrgID: ctx.Org.Organization.ID,
Type: tp,
})
}
t.Units = units
}
ctx.Data["Team"] = t
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplTeamNew)
return
}
if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
return
}
if err := models.NewTeam(t); err != nil {
ctx.Data["Err_TeamName"] = true
switch {
case models.IsErrTeamAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
default:
ctx.ServerError("NewTeam", err)
}
return
}
log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
}
// TeamMembers render team members page
func TeamMembers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamMembers"] = true
if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{}); err != nil {
ctx.ServerError("GetMembers", err)
return
}
ctx.HTML(http.StatusOK, tplTeamMembers)
}
// TeamRepositories show the repositories of team
func TeamRepositories(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamRepos"] = true
if err := ctx.Org.Team.GetRepositories(&models.SearchTeamOptions{}); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
ctx.HTML(http.StatusOK, tplTeamRepositories)
}
// EditTeam render team edit page
func EditTeam(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["team_name"] = ctx.Org.Team.Name
ctx.Data["desc"] = ctx.Org.Team.Description
ctx.Data["Units"] = models.Units
ctx.HTML(http.StatusOK, tplTeamNew)
}
// EditTeamPost response for modify team information
func EditTeamPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateTeamForm)
t := ctx.Org.Team
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["Team"] = t
ctx.Data["Units"] = models.Units
isAuthChanged := false
isIncludeAllChanged := false
var includesAllRepositories = form.RepoAccess == "all"
if !t.IsOwnerTeam() {
// Validate permission level.
auth := models.ParseAccessMode(form.Permission)
t.Name = form.TeamName
if t.Authorize != auth {
isAuthChanged = true
t.Authorize = auth
}
if t.IncludesAllRepositories != includesAllRepositories {
isIncludeAllChanged = true
t.IncludesAllRepositories = includesAllRepositories
}
}
t.Description = form.Description
if t.Authorize < models.AccessModeOwner {
var units = make([]models.TeamUnit, 0, len(form.Units))
for _, tp := range form.Units {
units = append(units, models.TeamUnit{
OrgID: t.OrgID,
TeamID: t.ID,
Type: tp,
})
}
err := models.UpdateTeamUnits(t, units)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err.Error())
return
}
}
t.CanCreateOrgRepo = form.CanCreateOrgRepo
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplTeamNew)
return
}
if t.Authorize < models.AccessModeAdmin && len(form.Units) == 0 {
ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
return
}
if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil {
ctx.Data["Err_TeamName"] = true
switch {
case models.IsErrTeamAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
default:
ctx.ServerError("UpdateTeam", err)
}
return
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
}
// DeleteTeam response for the delete team request
func DeleteTeam(ctx *context.Context) {
if err := models.DeleteTeam(ctx.Org.Team); err != nil {
ctx.Flash.Error("DeleteTeam: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/teams",
})
}

View File

@@ -0,0 +1,103 @@
// Copyright 2017 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 repo
import (
"net/http"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
)
const (
tplActivity base.TplName = "repo/activity"
)
// Activity render the page to show repository latest changes
func Activity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true
ctx.Data["Period"] = ctx.Params("period")
timeUntil := time.Now()
var timeFrom time.Time
switch ctx.Data["Period"] {
case "daily":
timeFrom = timeUntil.Add(-time.Hour * 24)
case "halfweekly":
timeFrom = timeUntil.Add(-time.Hour * 72)
case "weekly":
timeFrom = timeUntil.Add(-time.Hour * 168)
case "monthly":
timeFrom = timeUntil.AddDate(0, -1, 0)
case "quarterly":
timeFrom = timeUntil.AddDate(0, -3, 0)
case "semiyearly":
timeFrom = timeUntil.AddDate(0, -6, 0)
case "yearly":
timeFrom = timeUntil.AddDate(-1, 0, 0)
default:
ctx.Data["Period"] = "weekly"
timeFrom = timeUntil.Add(-time.Hour * 168)
}
ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006")
ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006")
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
var err error
if ctx.Data["Activity"], err = models.GetActivityStats(ctx.Repo.Repository, timeFrom,
ctx.Repo.CanRead(models.UnitTypeReleases),
ctx.Repo.CanRead(models.UnitTypeIssues),
ctx.Repo.CanRead(models.UnitTypePullRequests),
ctx.Repo.CanRead(models.UnitTypeCode)); err != nil {
ctx.ServerError("GetActivityStats", err)
return
}
if ctx.Data["ActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10); err != nil {
ctx.ServerError("GetActivityStatsTopAuthors", err)
return
}
ctx.HTML(http.StatusOK, tplActivity)
}
// ActivityAuthors renders JSON with top commit authors for given time period over all branches
func ActivityAuthors(ctx *context.Context) {
timeUntil := time.Now()
var timeFrom time.Time
switch ctx.Params("period") {
case "daily":
timeFrom = timeUntil.Add(-time.Hour * 24)
case "halfweekly":
timeFrom = timeUntil.Add(-time.Hour * 72)
case "weekly":
timeFrom = timeUntil.Add(-time.Hour * 168)
case "monthly":
timeFrom = timeUntil.AddDate(0, -1, 0)
case "quarterly":
timeFrom = timeUntil.AddDate(0, -3, 0)
case "semiyearly":
timeFrom = timeUntil.AddDate(0, -6, 0)
case "yearly":
timeFrom = timeUntil.AddDate(-1, 0, 0)
default:
timeFrom = timeUntil.Add(-time.Hour * 168)
}
var err error
authors, err := models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10)
if err != nil {
ctx.ServerError("GetActivityStatsTopAuthors", err)
return
}
ctx.JSON(http.StatusOK, authors)
}

View File

@@ -0,0 +1,160 @@
// Copyright 2017 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 repo
import (
"fmt"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/routers/common"
)
// UploadIssueAttachment response for Issue/PR attachments
func UploadIssueAttachment(ctx *context.Context) {
uploadAttachment(ctx, setting.Attachment.AllowedTypes)
}
// UploadReleaseAttachment response for uploading release attachments
func UploadReleaseAttachment(ctx *context.Context) {
uploadAttachment(ctx, setting.Repository.Release.AllowedTypes)
}
// UploadAttachment response for uploading attachments
func uploadAttachment(ctx *context.Context, allowedTypes string) {
if !setting.Attachment.Enabled {
ctx.Error(http.StatusNotFound, "attachment is not enabled")
return
}
file, header, err := ctx.Req.FormFile("file")
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
return
}
defer file.Close()
buf := make([]byte, 1024)
n, _ := file.Read(buf)
if n > 0 {
buf = buf[:n]
}
err = upload.Verify(buf, header.Filename, allowedTypes)
if err != nil {
ctx.Error(http.StatusBadRequest, err.Error())
return
}
attach, err := models.NewAttachment(&models.Attachment{
UploaderID: ctx.User.ID,
Name: header.Filename,
}, buf, file)
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
return
}
log.Trace("New attachment uploaded: %s", attach.UUID)
ctx.JSON(http.StatusOK, map[string]string{
"uuid": attach.UUID,
})
}
// DeleteAttachment response for deleting issue's attachment
func DeleteAttachment(ctx *context.Context) {
file := ctx.Query("file")
attach, err := models.GetAttachmentByUUID(file)
if err != nil {
ctx.Error(http.StatusBadRequest, err.Error())
return
}
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(http.StatusForbidden)
return
}
err = models.DeleteAttachment(attach, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
return
}
ctx.JSON(http.StatusOK, map[string]string{
"uuid": attach.UUID,
})
}
// GetAttachment serve attachements
func GetAttachment(ctx *context.Context) {
attach, err := models.GetAttachmentByUUID(ctx.Params(":uuid"))
if err != nil {
if models.IsErrAttachmentNotExist(err) {
ctx.Error(http.StatusNotFound)
} else {
ctx.ServerError("GetAttachmentByUUID", err)
}
return
}
repository, unitType, err := attach.LinkedRepository()
if err != nil {
ctx.ServerError("LinkedRepository", err)
return
}
if repository == nil { //If not linked
if !(ctx.IsSigned && attach.UploaderID == ctx.User.ID) { //We block if not the uploader
ctx.Error(http.StatusNotFound)
return
}
} else { //If we have the repository we check access
perm, err := models.GetUserRepoPermission(repository, ctx.User)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
return
}
if !perm.CanRead(unitType) {
ctx.Error(http.StatusNotFound)
return
}
}
if err := attach.IncreaseDownloadCount(); err != nil {
ctx.ServerError("IncreaseDownloadCount", err)
return
}
if setting.Attachment.ServeDirect {
//If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
return
}
//If we have matched and access to release or issue
fr, err := storage.Attachments.Open(attach.RelativePath())
if err != nil {
ctx.ServerError("Open", err)
return
}
defer fr.Close()
if err = common.ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
ctx.ServerError("ServeData", err)
return
}
}

251
routers/web/repo/blame.go Normal file
View File

@@ -0,0 +1,251 @@
// Copyright 2019 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 repo
import (
"bytes"
"container/list"
"fmt"
"html"
gotemplate "html/template"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
)
const (
tplBlame base.TplName = "repo/home"
)
// RefBlame render blame page
func RefBlame(ctx *context.Context) {
fileName := ctx.Repo.TreePath
if len(fileName) == 0 {
ctx.NotFound("Blame FileName", nil)
return
}
userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name
commitID := ctx.Repo.CommitID
commit, err := ctx.Repo.GitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return
}
if len(commitID) != 40 {
commitID = commit.ID.String()
}
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + ctx.Repo.TreePath
}
var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
}
}
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
latestCommit := ctx.Repo.Commit
if len(ctx.Repo.TreePath) > 0 {
latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
}
}
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
blob := entry.Blob()
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
ctx.Data["PageIsViewCode"] = true
ctx.Data["IsBlame"] = true
ctx.Data["FileSize"] = blob.Size()
ctx.Data["FileName"] = blob.Name()
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
if err != nil {
ctx.NotFound("GetBlobLineCount", err)
return
}
blameReader, err := git.CreateBlameReader(ctx, models.RepoPath(userName, repoName), commitID, fileName)
if err != nil {
ctx.NotFound("CreateBlameReader", err)
return
}
defer blameReader.Close()
blameParts := make([]git.BlamePart, 0)
for {
blamePart, err := blameReader.NextPart()
if err != nil {
ctx.NotFound("NextPart", err)
return
}
if blamePart == nil {
break
}
blameParts = append(blameParts, *blamePart)
}
commitNames := make(map[string]models.UserCommit)
commits := list.New()
for _, part := range blameParts {
sha := part.Sha
if _, ok := commitNames[sha]; ok {
continue
}
commit, err := ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return
}
commits.PushBack(commit)
commitNames[commit.ID.String()] = models.UserCommit{}
}
commits = models.ValidateCommitsWithEmails(commits)
for e := commits.Front(); e != nil; e = e.Next() {
c := e.Value.(models.UserCommit)
commitNames[c.ID.String()] = c
}
// Get Topics of this repo
renderRepoTopics(ctx)
if ctx.Written() {
return
}
renderBlame(ctx, blameParts, commitNames)
ctx.HTML(http.StatusOK, tplBlame)
}
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]models.UserCommit) {
repoLink := ctx.Repo.RepoLink
var lines = make([]string, 0)
var commitInfo bytes.Buffer
var lineNumbers bytes.Buffer
var codeLines bytes.Buffer
var i = 0
for pi, part := range blameParts {
for index, line := range part.Lines {
i++
lines = append(lines, line)
var attr = ""
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
attr = " bottom-line"
}
commit := commitNames[part.Sha]
if index == 0 {
// User avatar image
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))
var avatar string
if commit.User != nil {
avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
} else {
avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
}
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))
} else {
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s">&#8203;</div>`, attr))
}
//Line number
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d" class="bottom-line"></span>`, i, i))
} else {
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d"></span>`, i, i))
}
if i != len(lines)-1 {
line += "\n"
}
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
line = highlight.Code(fileName, line)
line = `<code class="code-inner">` + line + `</code>`
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
codeLines.WriteString(fmt.Sprintf(`<li class="L%d bottom-line" rel="L%d">%s</li>`, i, i, line))
} else {
codeLines.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, i, i, line))
}
}
}
ctx.Data["BlameContent"] = gotemplate.HTML(codeLines.String())
ctx.Data["BlameCommitInfo"] = gotemplate.HTML(commitInfo.String())
ctx.Data["BlameLineNums"] = gotemplate.HTML(lineNumbers.String())
}

407
routers/web/repo/branch.go Normal file
View File

@@ -0,0 +1,407 @@
// 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 repo
import (
"errors"
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repofiles"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
)
const (
tplBranch base.TplName = "repo/branch/list"
)
// Branch contains the branch information
type Branch struct {
Name string
Commit *git.Commit
IsProtected bool
IsDeleted bool
IsIncluded bool
DeletedBranch *models.DeletedBranch
CommitsAhead int
CommitsBehind int
LatestPullRequest *models.PullRequest
MergeMovedOn bool
}
// Branches render repository branch page
func Branches(ctx *context.Context) {
ctx.Data["Title"] = "Branches"
ctx.Data["IsRepoToolbarBranches"] = true
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls()
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(models.UnitTypeCode)
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
ctx.Data["CanPull"] = ctx.Repo.CanWrite(models.UnitTypeCode) || (ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID))
ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsBranches"] = true
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
limit := ctx.QueryInt("limit")
if limit <= 0 || limit > git.BranchesRangeSize {
limit = git.BranchesRangeSize
}
skip := (page - 1) * limit
log.Debug("Branches: skip: %d limit: %d", skip, limit)
branches, branchesCount := loadBranches(ctx, skip, limit)
if ctx.Written() {
return
}
ctx.Data["Branches"] = branches
pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplBranch)
}
// DeleteBranchPost responses for delete merged branch
func DeleteBranchPost(ctx *context.Context) {
defer redirect(ctx)
branchName := ctx.Query("name")
if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
case errors.Is(err, repo_service.ErrBranchIsDefault):
log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName))
case errors.Is(err, repo_service.ErrBranchIsProtected):
log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
default:
log.Error("DeleteBranch: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName))
}
return
}
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", branchName))
}
// RestoreBranchPost responses for delete merged branch
func RestoreBranchPost(ctx *context.Context) {
defer redirect(ctx)
branchID := ctx.QueryInt64("branch_id")
branchName := ctx.Query("name")
deletedBranch, err := ctx.Repo.Repository.GetDeletedBranchByID(branchID)
if err != nil {
log.Error("GetDeletedBranchByID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
return
}
if err := git.Push(ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(),
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name),
Env: models.PushingEnvironment(ctx.User, ctx.Repo.Repository),
}); err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name)
ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name))
return
}
log.Error("RestoreBranch: CreateBranch: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
return
}
// Don't return error below this
if err := repo_service.PushUpdate(
&repo_module.PushUpdateOptions{
RefFullName: git.BranchPrefix + deletedBranch.Name,
OldCommitID: git.EmptySHA,
NewCommitID: deletedBranch.Commit,
PusherID: ctx.User.ID,
PusherName: ctx.User.Name,
RepoUserName: ctx.Repo.Owner.Name,
RepoName: ctx.Repo.Repository.Name,
}); err != nil {
log.Error("RestoreBranch: Update: %v", err)
}
ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name))
}
func redirect(ctx *context.Context) {
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/branches",
})
}
// loadBranches loads branches from the repository limited by page & pageSize.
// NOTE: May write to context on error.
func loadBranches(ctx *context.Context, skip, limit int) ([]*Branch, int) {
defaultBranch, err := repo_module.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
if err != nil {
log.Error("loadBranches: get default branch: %v", err)
ctx.ServerError("GetDefaultBranch", err)
return nil, 0
}
rawBranches, totalNumOfBranches, err := repo_module.GetBranches(ctx.Repo.Repository, skip, limit)
if err != nil {
log.Error("GetBranches: %v", err)
ctx.ServerError("GetBranches", err)
return nil, 0
}
protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
if err != nil {
ctx.ServerError("GetProtectedBranches", err)
return nil, 0
}
repoIDToRepo := map[int64]*models.Repository{}
repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository
repoIDToGitRepo := map[int64]*git.Repository{}
repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo
var branches []*Branch
for i := range rawBranches {
if rawBranches[i].Name == defaultBranch.Name {
// Skip default branch
continue
}
var branch = loadOneBranch(ctx, rawBranches[i], protectedBranches, repoIDToRepo, repoIDToGitRepo)
if branch == nil {
return nil, 0
}
branches = append(branches, branch)
}
// Always add the default branch
log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name)
branches = append(branches, loadOneBranch(ctx, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo))
if ctx.Repo.CanWrite(models.UnitTypeCode) {
deletedBranches, err := getDeletedBranches(ctx)
if err != nil {
ctx.ServerError("getDeletedBranches", err)
return nil, 0
}
branches = append(branches, deletedBranches...)
}
return branches, totalNumOfBranches - 1
}
func loadOneBranch(ctx *context.Context, rawBranch *git.Branch, protectedBranches []*models.ProtectedBranch,
repoIDToRepo map[int64]*models.Repository,
repoIDToGitRepo map[int64]*git.Repository) *Branch {
log.Trace("loadOneBranch: '%s'", rawBranch.Name)
commit, err := rawBranch.GetCommit()
if err != nil {
ctx.ServerError("GetCommit", err)
return nil
}
branchName := rawBranch.Name
var isProtected bool
for _, b := range protectedBranches {
if b.BranchName == branchName {
isProtected = true
break
}
}
divergence, divergenceError := repofiles.CountDivergingCommits(ctx.Repo.Repository, git.BranchPrefix+branchName)
if divergenceError != nil {
ctx.ServerError("CountDivergingCommits", divergenceError)
return nil
}
pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
if err != nil {
ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
return nil
}
headCommit := commit.ID.String()
mergeMovedOn := false
if pr != nil {
pr.HeadRepo = ctx.Repo.Repository
if err := pr.LoadIssue(); err != nil {
ctx.ServerError("pr.LoadIssue", err)
return nil
}
if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
pr.BaseRepo = repo
} else if err := pr.LoadBaseRepo(); err != nil {
ctx.ServerError("pr.LoadBaseRepo", err)
return nil
} else {
repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
}
pr.Issue.Repo = pr.BaseRepo
if pr.HasMerged {
baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID]
if !ok {
baseGitRepo, err = git.OpenRepository(pr.BaseRepo.RepoPath())
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
}
defer baseGitRepo.Close()
repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo
}
pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil && !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommitID", err)
return nil
}
if err == nil && headCommit != pullCommit {
// the head has moved on from the merge - we shouldn't delete
mergeMovedOn = true
}
}
}
isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName
return &Branch{
Name: branchName,
Commit: commit,
IsProtected: isProtected,
IsIncluded: isIncluded,
CommitsAhead: divergence.Ahead,
CommitsBehind: divergence.Behind,
LatestPullRequest: pr,
MergeMovedOn: mergeMovedOn,
}
}
func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
branches := []*Branch{}
deletedBranches, err := ctx.Repo.Repository.GetDeletedBranches()
if err != nil {
return branches, err
}
for i := range deletedBranches {
deletedBranches[i].LoadUser()
branches = append(branches, &Branch{
Name: deletedBranches[i].Name,
IsDeleted: true,
DeletedBranch: deletedBranches[i],
})
}
return branches, nil
}
// CreateBranch creates new branch in repository
func CreateBranch(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewBranchForm)
if !ctx.Repo.CanCreateBranch() {
ctx.NotFound("CreateBranch", nil)
return
}
if ctx.HasError() {
ctx.Flash.Error(ctx.GetErrMsg())
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
var err error
if form.CreateTag {
if ctx.Repo.IsViewTag {
err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName, "")
} else {
err = release_service.CreateNewTag(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName, "")
}
} else if ctx.Repo.IsViewBranch {
err = repo_module.CreateNewBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
} else if ctx.Repo.IsViewTag {
err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
} else {
err = repo_module.CreateNewBranchFromCommit(ctx.User, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
}
if err != nil {
if models.IsErrTagAlreadyExists(err) {
e := err.(models.ErrTagAlreadyExists)
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
if models.IsErrBranchNameConflict(err) {
e := err.(models.ErrBranchNameConflict)
ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
if git.IsErrPushRejected(err) {
e := err.(*git.ErrPushRejected)
if len(e.Message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
} else {
flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(e.Message),
})
if err != nil {
ctx.ServerError("UpdatePullRequest.HTMLString", err)
return
}
ctx.Flash.Error(flashError)
}
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
ctx.ServerError("CreateNewBranch", err)
return
}
if form.CreateTag {
ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.NewBranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.NewBranchName))
return
}
ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName))
}

401
routers/web/repo/commit.go Normal file
View File

@@ -0,0 +1,401 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 repo
import (
"errors"
"net/http"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/gitdiff"
)
const (
tplCommits base.TplName = "repo/commits"
tplGraph base.TplName = "repo/graph"
tplGraphDiv base.TplName = "repo/graph/div"
tplCommitPage base.TplName = "repo/commit_page"
)
// RefCommits render commits page
func RefCommits(ctx *context.Context) {
switch {
case len(ctx.Repo.TreePath) == 0:
Commits(ctx)
case ctx.Repo.TreePath == "search":
SearchCommits(ctx)
default:
FileHistory(ctx)
}
}
// Commits render branch's commits
func Commits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true
if ctx.Repo.Commit == nil {
ctx.NotFound("Commit not found", nil)
return
}
ctx.Data["PageIsViewCode"] = true
commitsCount, err := ctx.Repo.GetCommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
pageSize := ctx.QueryInt("limit")
if pageSize <= 0 {
pageSize = git.CommitsRangeSize
}
// Both `git log branchName` and `git log commitId` work.
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize)
if err != nil {
ctx.ServerError("CommitsByRange", err)
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Branch"] = ctx.Repo.BranchName
pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
}
// Graph render commit graph - show commits from all branches.
func Graph(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.commit_graph")
ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
mode := strings.ToLower(ctx.QueryTrim("mode"))
if mode != "monochrome" {
mode = "color"
}
ctx.Data["Mode"] = mode
hidePRRefs := ctx.QueryBool("hide-pr-refs")
ctx.Data["HidePRRefs"] = hidePRRefs
branches := ctx.QueryStrings("branch")
realBranches := make([]string, len(branches))
copy(realBranches, branches)
for i, branch := range realBranches {
if strings.HasPrefix(branch, "--") {
realBranches[i] = "refs/heads/" + branch
}
}
ctx.Data["SelectedBranches"] = realBranches
files := ctx.QueryStrings("file")
commitsCount, err := ctx.Repo.GetCommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}
graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
if err != nil {
log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
realBranches = []string{}
branches = []string{}
graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(hidePRRefs, realBranches, files)
if err != nil {
ctx.ServerError("GetCommitGraphsCount", err)
return
}
}
page := ctx.QueryInt("page")
graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files)
if err != nil {
ctx.ServerError("GetCommitGraph", err)
return
}
if err := graph.LoadAndProcessCommits(ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil {
ctx.ServerError("LoadAndProcessCommits", err)
return
}
ctx.Data["Graph"] = graph
gitRefs, err := ctx.Repo.GitRepo.GetRefs()
if err != nil {
ctx.ServerError("GitRepo.GetRefs", err)
return
}
ctx.Data["AllRefs"] = gitRefs
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Branch"] = ctx.Repo.BranchName
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParam(ctx, "mode", "Mode")
paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
for _, branch := range branches {
paginator.AddParamString("branch", branch)
}
for _, file := range files {
paginator.AddParamString("file", file)
}
ctx.Data["Page"] = paginator
if ctx.QueryBool("div-only") {
ctx.HTML(http.StatusOK, tplGraphDiv)
return
}
ctx.HTML(http.StatusOK, tplGraph)
}
// SearchCommits render commits filtered by keyword
func SearchCommits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
query := strings.Trim(ctx.Query("q"), " ")
if len(query) == 0 {
ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL())
return
}
all := ctx.QueryBool("all")
opts := git.NewSearchCommitsOptions(query, all)
commits, err := ctx.Repo.Commit.SearchCommits(opts)
if err != nil {
ctx.ServerError("SearchCommits", err)
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["Keyword"] = query
if all {
ctx.Data["All"] = "checked"
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commits.Len()
ctx.Data["Branch"] = ctx.Repo.BranchName
ctx.HTML(http.StatusOK, tplCommits)
}
// FileHistory show a file's reversions
func FileHistory(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
fileName := ctx.Repo.TreePath
if len(fileName) == 0 {
Commits(ctx)
return
}
branchName := ctx.Repo.BranchName
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(branchName, fileName)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return
} else if commitsCount == 0 {
ctx.NotFound("FileCommitsCount", nil)
return
}
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(branchName, fileName, page)
if err != nil {
ctx.ServerError("CommitsByFileAndRange", err)
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["FileName"] = fileName
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Branch"] = branchName
pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
}
// Diff show different from current commit to previous commit
func Diff(ctx *context.Context) {
ctx.Data["PageIsDiff"] = true
ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["RequireTribute"] = true
userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name
commitID := ctx.Params(":sha")
var (
gitRepo *git.Repository
err error
repoPath string
)
if ctx.Data["PageIsWiki"] != nil {
gitRepo, err = git.OpenRepository(ctx.Repo.Repository.WikiPath())
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
}
repoPath = ctx.Repo.Repository.WikiPath()
} else {
gitRepo = ctx.Repo.GitRepo
repoPath = models.RepoPath(userName, repoName)
}
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return
}
if len(commitID) != 40 {
commitID = commit.ID.String()
}
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, commitID, models.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
ctx.Data["CommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["CommitStatuses"] = statuses
diff, err := gitdiff.GetDiffCommitWithWhitespaceBehavior(repoPath,
commitID, setting.Git.MaxGitDiffLines,
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if err != nil {
ctx.NotFound("GetDiffCommitWithWhitespaceBehavior", err)
return
}
parents := make([]string, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, err := commit.ParentID(i)
if err != nil {
ctx.NotFound("repo.Diff", err)
return
}
parents[i] = sha.String()
}
ctx.Data["CommitID"] = commitID
ctx.Data["AfterCommitID"] = commitID
ctx.Data["Username"] = userName
ctx.Data["Reponame"] = repoName
var parentCommit *git.Commit
if commit.ParentCount() > 0 {
parentCommit, err = gitRepo.GetCommit(parents[0])
if err != nil {
ctx.NotFound("GetParentCommit", err)
return
}
}
headTarget := path.Join(userName, repoName)
setCompareContext(ctx, parentCommit, commit, headTarget)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
verification := models.ParseCommitWithSignature(commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
ctx.Data["Diff"] = diff
ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
note := &git.Note{}
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
if err == nil {
ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = models.ValidateCommitWithEmail(note.Commit)
}
ctx.Data["BranchName"], err = commit.GetBranchName()
if err != nil {
ctx.ServerError("commit.GetBranchName", err)
return
}
ctx.Data["TagName"], err = commit.GetTagName()
if err != nil {
ctx.ServerError("commit.GetTagName", err)
return
}
ctx.HTML(http.StatusOK, tplCommitPage)
}
// RawDiff dumps diff results of repository in given commit ID to io.Writer
func RawDiff(ctx *context.Context) {
var repoPath string
if ctx.Data["PageIsWiki"] != nil {
repoPath = ctx.Repo.Repository.WikiPath()
} else {
repoPath = models.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
}
if err := git.GetRawDiff(
repoPath,
ctx.Params(":sha"),
git.RawDiffType(ctx.Params(":ext")),
ctx.Resp,
); err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetRawDiff",
errors.New("commit "+ctx.Params(":sha")+" does not exist."))
return
}
ctx.ServerError("GetRawDiff", err)
return
}
}

787
routers/web/repo/compare.go Normal file
View File

@@ -0,0 +1,787 @@
// Copyright 2019 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 repo
import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"html"
"net/http"
"path"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/services/gitdiff"
)
const (
tplCompare base.TplName = "repo/diff/compare"
tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
)
// setCompareContext sets context data.
func setCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
ctx.Data["BaseCommit"] = base
ctx.Data["HeadCommit"] = head
ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
if commit == nil {
return nil
}
blob, err := commit.GetBlobByPath(path)
if err != nil {
return nil
}
return blob
}
setPathsCompareContext(ctx, base, head, headTarget)
setImageCompareContext(ctx)
setCsvCompareContext(ctx)
}
// setPathsCompareContext sets context data for source and raw paths
func setPathsCompareContext(ctx *context.Context, base *git.Commit, head *git.Commit, headTarget string) {
sourcePath := setting.AppSubURL + "/%s/src/commit/%s"
rawPath := setting.AppSubURL + "/%s/raw/commit/%s"
ctx.Data["SourcePath"] = fmt.Sprintf(sourcePath, headTarget, head.ID)
ctx.Data["RawPath"] = fmt.Sprintf(rawPath, headTarget, head.ID)
if base != nil {
baseTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
ctx.Data["BeforeSourcePath"] = fmt.Sprintf(sourcePath, baseTarget, base.ID)
ctx.Data["BeforeRawPath"] = fmt.Sprintf(rawPath, baseTarget, base.ID)
}
}
// setImageCompareContext sets context data that is required by image compare template
func setImageCompareContext(ctx *context.Context) {
ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
if blob == nil {
return false
}
st, err := blob.GuessContentType()
if err != nil {
log.Error("GuessContentType failed: %v", err)
return false
}
return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
}
}
// setCsvCompareContext sets context data that is required by the CSV compare template
func setCsvCompareContext(ctx *context.Context) {
ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
extension := strings.ToLower(filepath.Ext(diffFile.Name))
return extension == ".csv" || extension == ".tsv"
}
type CsvDiffResult struct {
Sections []*gitdiff.TableDiffSection
Error string
}
ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseCommit *git.Commit, headCommit *git.Commit) CsvDiffResult {
if diffFile == nil || baseCommit == nil || headCommit == nil {
return CsvDiffResult{nil, ""}
}
errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
csvReaderFromCommit := func(c *git.Commit) (*csv.Reader, error) {
blob, err := c.GetBlobByPath(diffFile.Name)
if err != nil {
return nil, err
}
if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
return nil, errTooLarge
}
reader, err := blob.DataAsync()
if err != nil {
return nil, err
}
defer reader.Close()
return csv_module.CreateReaderAndGuessDelimiter(charset.ToUTF8WithFallbackReader(reader))
}
baseReader, err := csvReaderFromCommit(baseCommit)
if err == errTooLarge {
return CsvDiffResult{nil, err.Error()}
}
headReader, err := csvReaderFromCommit(headCommit)
if err == errTooLarge {
return CsvDiffResult{nil, err.Error()}
}
sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
if err != nil {
errMessage, err := csv_module.FormatError(err, ctx.Locale)
if err != nil {
log.Error("RenderCsvDiff failed: %v", err)
return CsvDiffResult{nil, ""}
}
return CsvDiffResult{nil, errMessage}
}
return CsvDiffResult{sections, ""}
}
}
// ParseCompareInfo parse compare info between two commit for preparing comparing references
func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *git.Repository, *git.CompareInfo, string, string) {
baseRepo := ctx.Repo.Repository
// Get compared branches information
// A full compare url is of the form:
//
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
//
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
// with the :baseRepo in ctx.Repo.
//
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
//
// How do we determine the :headRepo?
//
// 1. If :headOwner is not set then the :headRepo = :baseRepo
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
//
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
var (
headUser *models.User
headRepo *models.Repository
headBranch string
isSameRepo bool
infoPath string
err error
)
infoPath = ctx.Params("*")
infos := strings.SplitN(infoPath, "...", 2)
if len(infos) != 2 {
log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos)
ctx.NotFound("CompareAndPullRequest", nil)
return nil, nil, nil, nil, "", ""
}
ctx.Data["BaseName"] = baseRepo.OwnerName
baseBranch := infos[0]
ctx.Data["BaseBranch"] = baseBranch
// If there is no head repository, it means compare between same repository.
headInfos := strings.Split(infos[1], ":")
if len(headInfos) == 1 {
isSameRepo = true
headUser = ctx.Repo.Owner
headBranch = headInfos[0]
} else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 {
headUser, err = models.GetUserByName(headInfos[0])
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.NotFound("GetUserByName", nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil, nil, nil, nil, "", ""
}
headBranch = headInfos[1]
isSameRepo = headUser.ID == ctx.Repo.Owner.ID
if isSameRepo {
headRepo = baseRepo
}
} else {
headRepo, err = models.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
if err != nil {
if models.IsErrRepoNotExist(err) {
ctx.NotFound("GetRepositoryByOwnerAndName", nil)
} else {
ctx.ServerError("GetRepositoryByOwnerAndName", err)
}
return nil, nil, nil, nil, "", ""
}
if err := headRepo.GetOwner(); err != nil {
if models.IsErrUserNotExist(err) {
ctx.NotFound("GetUserByName", nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil, nil, nil, nil, "", ""
}
headBranch = headInfos[1]
headUser = headRepo.Owner
isSameRepo = headRepo.ID == ctx.Repo.Repository.ID
}
} else {
ctx.NotFound("CompareAndPullRequest", nil)
return nil, nil, nil, nil, "", ""
}
ctx.Data["HeadUser"] = headUser
ctx.Data["HeadBranch"] = headBranch
ctx.Repo.PullRequest.SameRepo = isSameRepo
// Check if base branch is valid.
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
baseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = baseBranch
baseIsCommit = true
} else {
ctx.NotFound("IsRefExist", nil)
return nil, nil, nil, nil, "", ""
}
}
ctx.Data["BaseIsCommit"] = baseIsCommit
ctx.Data["BaseIsBranch"] = baseIsBranch
ctx.Data["BaseIsTag"] = baseIsTag
ctx.Data["IsPull"] = true
// Now we have the repository that represents the base
// The current base and head repositories and branches may not
// actually be the intended branches that the user wants to
// create a pull-request from - but also determining the head
// repo is difficult.
// We will want therefore to offer a few repositories to set as
// our base and head
// 1. First if the baseRepo is a fork get the "RootRepo" it was
// forked from
var rootRepo *models.Repository
if baseRepo.IsFork {
err = baseRepo.GetBaseRepo()
if err != nil {
if !models.IsErrRepoNotExist(err) {
ctx.ServerError("Unable to find root repo", err)
return nil, nil, nil, nil, "", ""
}
} else {
rootRepo = baseRepo.BaseRepo
}
}
// 2. Now if the current user is not the owner of the baseRepo,
// check if they have a fork of the base repo and offer that as
// "OwnForkRepo"
var ownForkRepo *models.Repository
if ctx.User != nil && baseRepo.OwnerID != ctx.User.ID {
repo, has := models.HasForkedRepo(ctx.User.ID, baseRepo.ID)
if has {
ownForkRepo = repo
ctx.Data["OwnForkRepo"] = ownForkRepo
}
}
has := headRepo != nil
// 3. If the base is a forked from "RootRepo" and the owner of
// the "RootRepo" is the :headUser - set headRepo to that
if !has && rootRepo != nil && rootRepo.OwnerID == headUser.ID {
headRepo = rootRepo
has = true
}
// 4. If the ctx.User has their own fork of the baseRepo and the headUser is the ctx.User
// set the headRepo to the ownFork
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headUser.ID {
headRepo = ownForkRepo
has = true
}
// 5. If the headOwner has a fork of the baseRepo - use that
if !has {
headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID)
}
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
if !has && baseRepo.IsFork {
headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ForkID)
}
// 7. Otherwise if we're not the same repo and haven't found a repo give up
if !isSameRepo && !has {
ctx.Data["PageIsComparePull"] = false
}
// 8. Finally open the git repo
var headGitRepo *git.Repository
if isSameRepo {
headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo
} else if has {
headGitRepo, err = git.OpenRepository(headRepo.RepoPath())
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil, nil, nil, nil, "", ""
}
defer headGitRepo.Close()
}
ctx.Data["HeadRepo"] = headRepo
// Now we need to assert that the ctx.User has permission to read
// the baseRepo's code and pulls
// (NOT headRepo's)
permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil, nil, nil, nil, "", ""
}
if !permBase.CanRead(models.UnitTypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.User,
baseRepo,
permBase)
}
ctx.NotFound("ParseCompareInfo", nil)
return nil, nil, nil, nil, "", ""
}
// If we're not merging from the same repo:
if !isSameRepo {
// Assert ctx.User has permission to read headRepo's codes
permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil, nil, nil, nil, "", ""
}
if !permHead.CanRead(models.UnitTypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.User,
headRepo,
permHead)
}
ctx.NotFound("ParseCompareInfo", nil)
return nil, nil, nil, nil, "", ""
}
}
// If we have a rootRepo and it's different from:
// 1. the computed base
// 2. the computed head
// then get the branches of it
if rootRepo != nil &&
rootRepo.ID != headRepo.ID &&
rootRepo.ID != baseRepo.ID {
perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, rootRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return nil, nil, nil, nil, "", ""
}
if perm {
ctx.Data["RootRepo"] = rootRepo
ctx.Data["RootRepoBranches"] = branches
ctx.Data["RootRepoTags"] = tags
}
}
// If we have a ownForkRepo and it's different from:
// 1. The computed base
// 2. The computed head
// 3. The rootRepo (if we have one)
// then get the branches from it.
if ownForkRepo != nil &&
ownForkRepo.ID != headRepo.ID &&
ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
perm, branches, tags, err := getBranchesAndTagsForRepo(ctx.User, ownForkRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return nil, nil, nil, nil, "", ""
}
if perm {
ctx.Data["OwnForkRepo"] = ownForkRepo
ctx.Data["OwnForkRepoBranches"] = branches
ctx.Data["OwnForkRepoTags"] = tags
}
}
// Check if head branch is valid.
headIsCommit := headGitRepo.IsCommitExist(headBranch)
headIsBranch := headGitRepo.IsBranchExist(headBranch)
headIsTag := headGitRepo.IsTagExist(headBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash
if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
headBranch = headCommit.ID.String()
ctx.Data["HeadBranch"] = headBranch
headIsCommit = true
} else {
ctx.NotFound("IsRefExist", nil)
return nil, nil, nil, nil, "", ""
}
}
ctx.Data["HeadIsCommit"] = headIsCommit
ctx.Data["HeadIsBranch"] = headIsBranch
ctx.Data["HeadIsTag"] = headIsTag
// Treat as pull request if both references are branches
if ctx.Data["PageIsComparePull"] == nil {
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
}
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.User,
baseRepo,
permBase)
}
ctx.NotFound("ParseCompareInfo", nil)
return nil, nil, nil, nil, "", ""
}
baseBranchRef := baseBranch
if baseIsBranch {
baseBranchRef = git.BranchPrefix + baseBranch
} else if baseIsTag {
baseBranchRef = git.TagPrefix + baseBranch
}
headBranchRef := headBranch
if headIsBranch {
headBranchRef = git.BranchPrefix + headBranch
} else if headIsTag {
headBranchRef = git.TagPrefix + headBranch
}
compareInfo, err := headGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef)
if err != nil {
ctx.ServerError("GetCompareInfo", err)
return nil, nil, nil, nil, "", ""
}
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
}
// PrepareCompareDiff renders compare diff page
func PrepareCompareDiff(
ctx *context.Context,
headUser *models.User,
headRepo *models.Repository,
headGitRepo *git.Repository,
compareInfo *git.CompareInfo,
baseBranch, headBranch string,
whitespaceBehavior string) bool {
var (
repo = ctx.Repo.Repository
err error
title string
)
// Get diff information.
ctx.Data["CommitRepoLink"] = headRepo.Link()
headCommitID := compareInfo.HeadCommitID
ctx.Data["AfterCommitID"] = headCommitID
if headCommitID == compareInfo.MergeBase {
ctx.Data["IsNothingToCompare"] = true
if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
allowEmptyPr := !(baseBranch == headBranch && ctx.Repo.Repository.Name == headRepo.Name)
ctx.Data["AllowEmptyPr"] = allowEmptyPr
return !allowEmptyPr
}
ctx.Data["AllowEmptyPr"] = false
}
return true
}
diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(models.RepoPath(headUser.Name, headRepo.Name),
compareInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines,
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, whitespaceBehavior)
if err != nil {
ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
return false
}
ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
headCommit, err := headGitRepo.GetCommit(headCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return false
}
baseGitRepo := ctx.Repo.GitRepo
baseCommitID := compareInfo.BaseCommitID
baseCommit, err := baseGitRepo.GetCommit(baseCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return false
}
compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
ctx.Data["Commits"] = compareInfo.Commits
ctx.Data["CommitCount"] = compareInfo.Commits.Len()
if compareInfo.Commits.Len() == 1 {
c := compareInfo.Commits.Front().Value.(models.SignCommitWithStatuses)
title = strings.TrimSpace(c.UserCommit.Summary())
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
} else {
title = headBranch
}
ctx.Data["title"] = title
ctx.Data["Username"] = headUser.Name
ctx.Data["Reponame"] = headRepo.Name
headTarget := path.Join(headUser.Name, repo.Name)
setCompareContext(ctx, baseCommit, headCommit, headTarget)
return false
}
func getBranchesAndTagsForRepo(user *models.User, repo *models.Repository) (bool, []string, []string, error) {
perm, err := models.GetUserRepoPermission(repo, user)
if err != nil {
return false, nil, nil, err
}
if !perm.CanRead(models.UnitTypeCode) {
return false, nil, nil, nil
}
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
branches, _, err := gitRepo.GetBranches(0, 0)
if err != nil {
return false, nil, nil, err
}
tags, err := gitRepo.GetTags()
if err != nil {
return false, nil, nil, err
}
return true, branches, tags, nil
}
// CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) {
headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
if ctx.Written() {
return
}
defer headGitRepo.Close()
nothingToCompare := PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch,
gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() {
return
}
baseGitRepo := ctx.Repo.GitRepo
baseTags, err := baseGitRepo.GetTags()
if err != nil {
ctx.ServerError("GetTags", err)
return
}
ctx.Data["Tags"] = baseTags
headBranches, _, err := headGitRepo.GetBranches(0, 0)
if err != nil {
ctx.ServerError("GetBranches", err)
return
}
ctx.Data["HeadBranches"] = headBranches
headTags, err := headGitRepo.GetTags()
if err != nil {
ctx.ServerError("GetTags", err)
return
}
ctx.Data["HeadTags"] = headTags
if ctx.Data["PageIsComparePull"] == true {
pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch)
if err != nil {
if !models.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
} else {
ctx.Data["HasPullRequest"] = true
ctx.Data["PullRequest"] = pr
ctx.HTML(http.StatusOK, tplCompareDiff)
return
}
if !nothingToCompare {
// Setup information for new form.
RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
if ctx.Written() {
return
}
}
}
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
afterCommitID := ctx.Data["AfterCommitID"].(string)
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + "..." + base.ShortSha(afterCommitID)
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypePullRequests)
ctx.HTML(http.StatusOK, tplCompare)
}
// ExcerptBlob render blob excerpt contents
func ExcerptBlob(ctx *context.Context) {
commitID := ctx.Params("sha")
lastLeft := ctx.QueryInt("last_left")
lastRight := ctx.QueryInt("last_right")
idxLeft := ctx.QueryInt("left")
idxRight := ctx.QueryInt("right")
leftHunkSize := ctx.QueryInt("left_hunk_size")
rightHunkSize := ctx.QueryInt("right_hunk_size")
anchor := ctx.Query("anchor")
direction := ctx.Query("direction")
filePath := ctx.Query("path")
gitRepo := ctx.Repo.GitRepo
chunkSize := gitdiff.BlobExcerptChunkSize
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommit")
return
}
section := &gitdiff.DiffSection{
FileName: filePath,
Name: filePath,
}
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
idxLeft -= chunkSize
idxRight -= chunkSize
leftHunkSize += chunkSize
rightHunkSize += chunkSize
section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
lastLeft += chunkSize
lastRight += chunkSize
} else {
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight-1)
leftHunkSize = 0
rightHunkSize = 0
idxLeft = lastLeft
idxRight = lastRight
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "getExcerptLines")
return
}
if idxRight > lastRight {
lineText := " "
if rightHunkSize > 0 || leftHunkSize > 0 {
lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
}
lineText = html.EscapeString(lineText)
lineSection := &gitdiff.DiffLine{
Type: gitdiff.DiffLineSection,
Content: lineText,
SectionInfo: &gitdiff.DiffLineSectionInfo{
Path: filePath,
LastLeftIdx: lastLeft,
LastRightIdx: lastRight,
LeftIdx: idxLeft,
RightIdx: idxRight,
LeftHunkSize: leftHunkSize,
RightHunkSize: rightHunkSize,
}}
if direction == "up" {
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
} else if direction == "down" {
section.Lines = append(section.Lines, lineSection)
}
}
ctx.Data["section"] = section
ctx.Data["fileName"] = filePath
ctx.Data["AfterCommitID"] = commitID
ctx.Data["Anchor"] = anchor
ctx.HTML(http.StatusOK, tplBlobExcerpt)
}
func getExcerptLines(commit *git.Commit, filePath string, idxLeft int, idxRight int, chunkSize int) ([]*gitdiff.DiffLine, error) {
blob, err := commit.Tree.GetBlobByPath(filePath)
if err != nil {
return nil, err
}
reader, err := blob.DataAsync()
if err != nil {
return nil, err
}
defer reader.Close()
scanner := bufio.NewScanner(reader)
var diffLines []*gitdiff.DiffLine
for line := 0; line < idxRight+chunkSize; line++ {
if ok := scanner.Scan(); !ok {
break
}
if line < idxRight {
continue
}
lineText := scanner.Text()
diffLine := &gitdiff.DiffLine{
LeftIdx: idxLeft + (line - idxRight) + 1,
RightIdx: line + 1,
Type: gitdiff.DiffLinePlain,
Content: " " + lineText,
}
diffLines = append(diffLines, diffLine)
}
return diffLines, nil
}

View File

@@ -0,0 +1,131 @@
// 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 repo
import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/common"
)
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
return nil
}
dataRc, err := blob.DataAsync()
if err != nil {
return err
}
closed := false
defer func() {
if closed {
return
}
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
}()
pointer, _ := lfs.ReadPointer(dataRc)
if pointer.IsValid() {
meta, _ := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
if meta == nil {
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
return common.ServeBlob(ctx, blob)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
return nil
}
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
return err
}
defer func() {
if err = lfsDataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
}()
return common.ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
}
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
return common.ServeBlob(ctx, blob)
}
// SingleDownload download a file by repos path
func SingleDownload(ctx *context.Context) {
blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetBlobByPath", nil)
} else {
ctx.ServerError("GetBlobByPath", err)
}
return
}
if err = common.ServeBlob(ctx, blob); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
func SingleDownloadOrLFS(ctx *context.Context) {
blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetBlobByPath", nil)
} else {
ctx.ServerError("GetBlobByPath", err)
}
return
}
if err = ServeBlobOrLFS(ctx, blob); err != nil {
ctx.ServerError("ServeBlobOrLFS", err)
}
}
// DownloadByID download a file by sha1 ID
func DownloadByID(ctx *context.Context) {
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetBlob", nil)
} else {
ctx.ServerError("GetBlob", err)
}
return
}
if err = common.ServeBlob(ctx, blob); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
func DownloadByIDOrLFS(ctx *context.Context) {
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.Params("sha"))
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetBlob", nil)
} else {
ctx.ServerError("GetBlob", err)
}
return
}
if err = ServeBlobOrLFS(ctx, blob); err != nil {
ctx.ServerError("ServeBlob", err)
}
}

831
routers/web/repo/editor.go Normal file
View File

@@ -0,0 +1,831 @@
// Copyright 2016 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 repo
import (
"fmt"
"io/ioutil"
"net/http"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repofiles"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
jsoniter "github.com/json-iterator/go"
)
const (
tplEditFile base.TplName = "repo/editor/edit"
tplEditDiffPreview base.TplName = "repo/editor/diff_preview"
tplDeleteFile base.TplName = "repo/editor/delete"
tplUploadFile base.TplName = "repo/editor/upload"
frmCommitChoiceDirect string = "direct"
frmCommitChoiceNewBranch string = "commit-to-new-branch"
)
func renderCommitRights(ctx *context.Context) bool {
canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx.User)
if err != nil {
log.Error("CanCommitToBranch: %v", err)
}
ctx.Data["CanCommitToBranch"] = canCommitToBranch
return canCommitToBranch.CanCommitToBranch
}
// getParentTreeFields returns list of parent tree names and corresponding tree paths
// based on given tree path.
func getParentTreeFields(treePath string) (treeNames []string, treePaths []string) {
if len(treePath) == 0 {
return treeNames, treePaths
}
treeNames = strings.Split(treePath, "/")
treePaths = make([]string, len(treeNames))
for i := range treeNames {
treePaths[i] = strings.Join(treeNames[:i+1], "/")
}
return treeNames, treePaths
}
func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile
ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireSimpleMDE"] = true
canCommit := renderCommitRights(ctx)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
if treePath != ctx.Repo.TreePath {
if isNewFile {
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
} else {
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
}
return
}
treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
if !isNewFile {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
// No way to edit a directory online.
if entry.IsDir() {
ctx.NotFound("entry.IsDir", nil)
return
}
blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize {
ctx.NotFound("blob.Size", err)
return
}
dataRc, err := blob.DataAsync()
if err != nil {
ctx.NotFound("blob.Data", err)
return
}
defer dataRc.Close()
ctx.Data["FileSize"] = blob.Size()
ctx.Data["FileName"] = blob.Name()
buf := make([]byte, 1024)
n, _ := dataRc.Read(buf)
buf = buf[:n]
// Only some file types are editable online as text.
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
return
}
d, _ := ioutil.ReadAll(dataRc)
if err := dataRc.Close(); err != nil {
log.Error("Error whilst closing blob data: %v", err)
}
buf = append(buf, d...)
if content, err := charset.ToUTF8WithErr(buf); err != nil {
log.Error("ToUTF8WithErr: %v", err)
ctx.Data["FileContent"] = string(buf)
} else {
ctx.Data["FileContent"] = content
}
} else {
treeNames = append(treeNames, "") // Append empty string to allow user name the new file.
}
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
if canCommit {
ctx.Data["commit_choice"] = frmCommitChoiceDirect
} else {
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
}
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.Data["last_commit"] = ctx.Repo.CommitID
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)
ctx.HTML(http.StatusOK, tplEditFile)
}
// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
func GetEditorConfig(ctx *context.Context, treePath string) string {
ec, err := ctx.Repo.GetEditorconfig()
if err == nil {
def, err := ec.GetDefinitionForFilename(treePath)
if err == nil {
json := jsoniter.ConfigCompatibleWithStandardLibrary
jsonStr, _ := json.Marshal(def)
return string(jsonStr)
}
}
return "null"
}
// EditFile render edit file page
func EditFile(ctx *context.Context) {
editFile(ctx, false)
}
// NewFile render create file page
func NewFile(ctx *context.Context) {
editFile(ctx, true)
}
func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
canCommit := renderCommitRights(ctx)
treeNames, treePaths := getParentTreeFields(form.TreePath)
branchName := ctx.Repo.BranchName
if form.CommitChoice == frmCommitChoiceNewBranch {
branchName = form.NewBranchName
}
ctx.Data["PageIsEdit"] = true
ctx.Data["PageHasPosted"] = true
ctx.Data["IsNewFile"] = isNewFile
ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["TreePath"] = form.TreePath
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
ctx.Data["FileContent"] = form.Content
ctx.Data["commit_summary"] = form.CommitSummary
ctx.Data["commit_message"] = form.CommitMessage
ctx.Data["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = form.NewBranchName
ctx.Data["last_commit"] = ctx.Repo.CommitID
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, form.TreePath)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplEditFile)
return
}
// Cannot commit to a an existing branch if user doesn't have rights
if branchName == ctx.Repo.BranchName && !canCommit {
ctx.Data["Err_NewBranchName"] = true
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
return
}
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
// `message` will be both the summary and message combined
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
if isNewFile {
message = ctx.Tr("repo.editor.add", form.TreePath)
} else {
message = ctx.Tr("repo.editor.update", form.TreePath)
}
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
message += "\n\n" + form.CommitMessage
}
if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: branchName,
FromTreePath: ctx.Repo.TreePath,
TreePath: form.TreePath,
Message: message,
Content: strings.ReplaceAll(form.Content, "\r", ""),
IsNewFile: isNewFile,
Signoff: form.Signoff,
}); err != nil {
// This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
if git.IsErrNotExist(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
} else if models.IsErrLFSFileLocked(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplEditFile, &form)
} else if models.IsErrFilenameInvalid(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
} else if models.IsErrFilePathInvalid(err) {
ctx.Data["Err_TreePath"] = true
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
switch fileErr.Type {
case git.EntryModeSymlink:
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
case git.EntryModeTree:
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
case git.EntryModeBlob:
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
default:
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrRepoFileAlreadyExists(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
} else if git.IsErrBranchNotExist(err) {
// For when a user adds/updates a file to a branch that no longer exists
if branchErr, ok := err.(git.ErrBranchNotExist); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrBranchAlreadyExists(err) {
// For when a user specifies a new branch that already exists
ctx.Data["Err_NewBranchName"] = true
if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
} else if git.IsErrPushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else {
flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
})
if err != nil {
ctx.ServerError("editFilePost.HTMLString", err)
return
}
ctx.RenderWithErr(flashError, tplEditFile, &form)
}
} else {
flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
"Details": utils.SanitizeFlashErrorString(err.Error()),
})
if err != nil {
ctx.ServerError("editFilePost.HTMLString", err)
return
}
ctx.RenderWithErr(flashError, tplEditFile, &form)
}
}
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
}
}
// EditFilePost response for editing file
func EditFilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
editFilePost(ctx, *form, false)
}
// NewFilePost response for creating file
func NewFilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditRepoFileForm)
editFilePost(ctx, *form, true)
}
// DiffPreviewPost render preview diff page
func DiffPreviewPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
if len(treePath) == 0 {
ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
return
}
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
return
} else if entry.IsDir() {
ctx.Error(http.StatusUnprocessableEntity)
return
}
diff, err := repofiles.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
return
}
if diff.NumFiles == 0 {
ctx.PlainText(200, []byte(ctx.Tr("repo.editor.no_changes_to_show")))
return
}
ctx.Data["File"] = diff.Files[0]
ctx.HTML(http.StatusOK, tplEditDiffPreview)
}
// DeleteFile render delete file page
func DeleteFile(ctx *context.Context) {
ctx.Data["PageIsDelete"] = true
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treePath := cleanUploadFileName(ctx.Repo.TreePath)
if treePath != ctx.Repo.TreePath {
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
return
}
ctx.Data["TreePath"] = treePath
canCommit := renderCommitRights(ctx)
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
ctx.Data["last_commit"] = ctx.Repo.CommitID
if canCommit {
ctx.Data["commit_choice"] = frmCommitChoiceDirect
} else {
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
}
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.HTML(http.StatusOK, tplDeleteFile)
}
// DeleteFilePost response for deleting file
func DeleteFilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
canCommit := renderCommitRights(ctx)
branchName := ctx.Repo.BranchName
if form.CommitChoice == frmCommitChoiceNewBranch {
branchName = form.NewBranchName
}
ctx.Data["PageIsDelete"] = true
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["TreePath"] = ctx.Repo.TreePath
ctx.Data["commit_summary"] = form.CommitSummary
ctx.Data["commit_message"] = form.CommitMessage
ctx.Data["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = form.NewBranchName
ctx.Data["last_commit"] = ctx.Repo.CommitID
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplDeleteFile)
return
}
if branchName == ctx.Repo.BranchName && !canCommit {
ctx.Data["Err_NewBranchName"] = true
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
return
}
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
message += "\n\n" + form.CommitMessage
}
if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: branchName,
TreePath: ctx.Repo.TreePath,
Message: message,
Signoff: form.Signoff,
}); err != nil {
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
} else if models.IsErrFilenameInvalid(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
} else if models.IsErrFilePathInvalid(err) {
ctx.Data["Err_TreePath"] = true
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
switch fileErr.Type {
case git.EntryModeSymlink:
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
case git.EntryModeTree:
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
case git.EntryModeBlob:
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
default:
ctx.ServerError("DeleteRepoFile", err)
}
} else {
ctx.ServerError("DeleteRepoFile", err)
}
} else if git.IsErrBranchNotExist(err) {
// For when a user deletes a file to a branch that no longer exists
if branchErr, ok := err.(git.ErrBranchNotExist); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrBranchAlreadyExists(err) {
// For when a user specifies a new branch that already exists
if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
} else {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else {
flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
})
if err != nil {
ctx.ServerError("DeleteFilePost.HTMLString", err)
return
}
ctx.RenderWithErr(flashError, tplDeleteFile, &form)
}
} else {
ctx.ServerError("DeleteRepoFile", err)
}
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
treePath := path.Dir(ctx.Repo.TreePath)
if treePath == "." {
treePath = "" // the file deleted was in the root, so we return the user to the root directory
}
if len(treePath) > 0 {
// Need to get the latest commit since it changed
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
if err == nil && commit != nil {
// We have the comment, now find what directory we can return the user to
// (must have entries)
treePath = GetClosestParentWithFiles(treePath, commit)
} else {
treePath = "" // otherwise return them to the root of the repo
}
}
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(treePath))
}
}
// UploadFile render upload file page
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["RequireSimpleMDE"] = true
upload.AddUploadContext(ctx, "repo")
canCommit := renderCommitRights(ctx)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
if treePath != ctx.Repo.TreePath {
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
return
}
ctx.Repo.TreePath = treePath
treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
if len(treeNames) == 0 {
// We must at least have one element for user to input.
treeNames = []string{""}
}
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
if canCommit {
ctx.Data["commit_choice"] = frmCommitChoiceDirect
} else {
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
}
ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
ctx.HTML(http.StatusOK, tplUploadFile)
}
// UploadFilePost response for uploading file
func UploadFilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
ctx.Data["PageIsUpload"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["RequireSimpleMDE"] = true
upload.AddUploadContext(ctx, "repo")
canCommit := renderCommitRights(ctx)
oldBranchName := ctx.Repo.BranchName
branchName := oldBranchName
if form.CommitChoice == frmCommitChoiceNewBranch {
branchName = form.NewBranchName
}
form.TreePath = cleanUploadFileName(form.TreePath)
treeNames, treePaths := getParentTreeFields(form.TreePath)
if len(treeNames) == 0 {
// We must at least have one element for user to input.
treeNames = []string{""}
}
ctx.Data["TreePath"] = form.TreePath
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
ctx.Data["commit_summary"] = form.CommitSummary
ctx.Data["commit_message"] = form.CommitMessage
ctx.Data["commit_choice"] = form.CommitChoice
ctx.Data["new_branch_name"] = branchName
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplUploadFile)
return
}
if oldBranchName != branchName {
if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err == nil {
ctx.Data["Err_NewBranchName"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
return
}
} else if !canCommit {
ctx.Data["Err_NewBranchName"] = true
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
return
}
var newTreePath string
for _, part := range treeNames {
newTreePath = path.Join(newTreePath, part)
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
if err != nil {
if git.IsErrNotExist(err) {
// Means there is no item with that name, so we're good
break
}
ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
return
}
// User can only upload files to a directory.
if !entry.IsDir() {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
return
}
}
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
message = ctx.Tr("repo.editor.upload_files_to_dir", form.TreePath)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
message += "\n\n" + form.CommitMessage
}
if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
LastCommitID: ctx.Repo.CommitID,
OldBranch: oldBranchName,
NewBranch: branchName,
TreePath: form.TreePath,
Message: message,
Files: form.Files,
Signoff: form.Signoff,
}); err != nil {
if models.IsErrLFSFileLocked(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplUploadFile, &form)
} else if models.IsErrFilenameInvalid(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
} else if models.IsErrFilePathInvalid(err) {
ctx.Data["Err_TreePath"] = true
fileErr := err.(models.ErrFilePathInvalid)
switch fileErr.Type {
case git.EntryModeSymlink:
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
case git.EntryModeTree:
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
case git.EntryModeBlob:
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
default:
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrRepoFileAlreadyExists(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
} else if git.IsErrBranchNotExist(err) {
branchErr := err.(git.ErrBranchNotExist)
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
} else if models.IsErrBranchAlreadyExists(err) {
// For when a user specifies a new branch that already exists
ctx.Data["Err_NewBranchName"] = true
branchErr := err.(models.ErrBranchAlreadyExists)
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
} else if git.IsErrPushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+ctx.Repo.CommitID+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
} else {
flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
})
if err != nil {
ctx.ServerError("UploadFilePost.HTMLString", err)
return
}
ctx.RenderWithErr(flashError, tplUploadFile, &form)
}
} else {
// os.ErrNotExist - upload file missing in the intervening time?!
log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
}
return
}
if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
}
}
func cleanUploadFileName(name string) string {
// Rebase the filename
name = strings.Trim(path.Clean("/"+name), " /")
// Git disallows any filenames to have a .git directory in them.
for _, part := range strings.Split(name, "/") {
if strings.ToLower(part) == ".git" {
return ""
}
}
return name
}
// UploadFileToServer upload file to server file dir not git
func UploadFileToServer(ctx *context.Context) {
file, header, err := ctx.Req.FormFile("file")
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
return
}
defer file.Close()
buf := make([]byte, 1024)
n, _ := file.Read(buf)
if n > 0 {
buf = buf[:n]
}
err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
if err != nil {
ctx.Error(http.StatusBadRequest, err.Error())
return
}
name := cleanUploadFileName(header.Filename)
if len(name) == 0 {
ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
return
}
upload, err := models.NewUpload(name, buf, file)
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
return
}
log.Trace("New file uploaded: %s", upload.UUID)
ctx.JSON(http.StatusOK, map[string]string{
"uuid": upload.UUID,
})
}
// RemoveUploadFileFromServer remove file from server file dir
func RemoveUploadFileFromServer(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
if len(form.File) == 0 {
ctx.Status(204)
return
}
if err := models.DeleteUploadByUUID(form.File); err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
return
}
log.Trace("Upload file removed: %s", form.File)
ctx.Status(204)
}
// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
// type in the branch name themselves (will be an empty field)
func GetUniquePatchBranchName(ctx *context.Context) string {
prefix := ctx.User.LowerName + "-patch-"
for i := 1; i <= 1000; i++ {
branchName := fmt.Sprintf("%s%d", prefix, i)
if _, err := repo_module.GetBranch(ctx.Repo.Repository, branchName); err != nil {
if git.IsErrBranchNotExist(err) {
return branchName
}
log.Error("GetUniquePatchBranchName: %v", err)
return ""
}
}
return ""
}
// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
if len(treePath) == 0 || treePath == "." {
return ""
}
// see if the tree has entries
if tree, err := commit.SubTree(treePath); err != nil {
// failed to get tree, going up a dir
return GetClosestParentWithFiles(path.Dir(treePath), commit)
} else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
// no files in this dir, going up a dir
return GetClosestParentWithFiles(path.Dir(treePath), commit)
}
return treePath
}

View File

@@ -0,0 +1,83 @@
// 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 repo
import (
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestCleanUploadName(t *testing.T) {
models.PrepareTestEnv(t)
var kases = map[string]string{
".git/refs/master": "",
"/root/abc": "root/abc",
"./../../abc": "abc",
"a/../.git": "",
"a/../../../abc": "abc",
"../../../acd": "acd",
"../../.git/abc": "",
"..\\..\\.git/abc": "..\\..\\.git/abc",
"..\\../.git/abc": "",
"..\\../.git": "",
"abc/../def": "def",
".drone.yml": ".drone.yml",
".abc/def/.drone.yml": ".abc/def/.drone.yml",
"..drone.yml.": "..drone.yml.",
"..a.dotty...name...": "..a.dotty...name...",
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
}
for k, v := range kases {
assert.EqualValues(t, cleanUploadFileName(k), v)
}
}
func TestGetUniquePatchBranchName(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1")
ctx.SetParams(":id", "1")
test.LoadRepo(t, ctx, 1)
test.LoadRepoCommit(t, ctx)
test.LoadUser(t, ctx, 2)
test.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
expectedBranchName := "user2-patch-1"
branchName := GetUniquePatchBranchName(ctx)
assert.Equal(t, expectedBranchName, branchName)
}
func TestGetClosestParentWithFiles(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1")
ctx.SetParams(":id", "1")
test.LoadRepo(t, ctx, 1)
test.LoadRepoCommit(t, ctx)
test.LoadUser(t, ctx, 2)
test.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
repo := ctx.Repo.Repository
branch := repo.DefaultBranch
gitRepo, _ := git.OpenRepository(repo.RepoPath())
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(branch)
expectedTreePath := ""
expectedTreePath = "" // Should return the root dir, empty string, since there are no subdirs in this repo
for _, deletedFile := range []string{
"dir1/dir2/dir3/file.txt",
"file.txt",
} {
treePath := GetClosestParentWithFiles(deletedFile, commit)
assert.Equal(t, expectedTreePath, treePath)
}
}

602
routers/web/repo/http.go Normal file
View File

@@ -0,0 +1,602 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 repo
import (
"bytes"
"compress/gzip"
gocontext "context"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
)
// httpBase implmentation git smart HTTP protocol
func httpBase(ctx *context.Context) (h *serviceHandler) {
if setting.Repository.DisableHTTPGit {
ctx.Resp.WriteHeader(http.StatusForbidden)
_, err := ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed"))
if err != nil {
log.Error(err.Error())
}
return
}
if len(setting.Repository.AccessControlAllowOrigin) > 0 {
allowedOrigin := setting.Repository.AccessControlAllowOrigin
// Set CORS headers for browser-based git clients
ctx.Resp.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
// Handle preflight OPTIONS request
if ctx.Req.Method == "OPTIONS" {
if allowedOrigin == "*" {
ctx.Status(http.StatusOK)
} else if allowedOrigin == "null" {
ctx.Status(http.StatusForbidden)
} else {
origin := ctx.Req.Header.Get("Origin")
if len(origin) > 0 && origin == allowedOrigin {
ctx.Status(http.StatusOK)
} else {
ctx.Status(http.StatusForbidden)
}
}
return
}
}
username := ctx.Params(":username")
reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
if ctx.Query("go-get") == "1" {
context.EarlyResponseForGoGetMeta(ctx)
return
}
var isPull, receivePack bool
service := ctx.Query("service")
if service == "git-receive-pack" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
isPull = false
receivePack = true
} else if service == "git-upload-pack" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
isPull = true
} else if service == "git-upload-archive" ||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true
} else {
isPull = ctx.Req.Method == "GET"
}
var accessMode models.AccessMode
if isPull {
accessMode = models.AccessModeRead
} else {
accessMode = models.AccessModeWrite
}
isWiki := false
var unitType = models.UnitTypeCode
var wikiRepoName string
if strings.HasSuffix(reponame, ".wiki") {
isWiki = true
unitType = models.UnitTypeWiki
wikiRepoName = reponame
reponame = reponame[:len(reponame)-5]
}
owner, err := models.GetUserByName(username)
if err != nil {
if models.IsErrUserNotExist(err) {
if redirectUserID, err := models.LookupUserRedirect(username); err == nil {
context.RedirectToUser(ctx, username, redirectUserID)
} else {
ctx.NotFound(fmt.Sprintf("User %s does not exist", username), nil)
}
} else {
ctx.ServerError("GetUserByName", err)
}
return
}
if !owner.IsOrganization() && !owner.IsActive {
ctx.HandleText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.")
return
}
repoExist := true
repo, err := models.GetRepositoryByName(owner.ID, reponame)
if err != nil {
if models.IsErrRepoNotExist(err) {
if redirectRepoID, err := models.LookupRepoRedirect(owner.ID, reponame); err == nil {
context.RedirectToRepo(ctx, redirectRepoID)
return
}
repoExist = false
} else {
ctx.ServerError("GetRepositoryByName", err)
return
}
}
// Don't allow pushing if the repo is archived
if repoExist && repo.IsArchived && !isPull {
ctx.HandleText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.")
return
}
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
askAuth = !isPublicPull || setting.Service.RequireSignInView
environ []string
)
// don't allow anonymous pulls if organization is not public
if isPublicPull {
if err := repo.GetOwner(); err != nil {
ctx.ServerError("GetOwner", err)
return
}
askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic)
}
// check access
if askAuth {
// rely on the results of Contexter
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
ctx.Error(http.StatusUnauthorized)
return
}
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true {
_, err = models.GetTwoFactorByUID(ctx.User.ID)
if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
ctx.HandleText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page")
return
} else if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("IsErrTwoFactorNotEnrolled", err)
return
}
}
if !ctx.User.IsActive || ctx.User.ProhibitLogin {
ctx.HandleText(http.StatusForbidden, "Your account is disabled.")
return
}
if repoExist {
perm, err := models.GetUserRepoPermission(repo, ctx.User)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if !perm.CanAccess(accessMode, unitType) {
ctx.HandleText(http.StatusForbidden, "User permission denied")
return
}
if !isPull && repo.IsMirror {
ctx.HandleText(http.StatusForbidden, "mirror repository is read-only")
return
}
}
environ = []string{
models.EnvRepoUsername + "=" + username,
models.EnvRepoName + "=" + reponame,
models.EnvPusherName + "=" + ctx.User.Name,
models.EnvPusherID + fmt.Sprintf("=%d", ctx.User.ID),
models.EnvIsDeployKey + "=false",
models.EnvAppURL + "=" + setting.AppURL,
}
if !ctx.User.KeepEmailPrivate {
environ = append(environ, models.EnvPusherEmail+"="+ctx.User.Email)
}
if isWiki {
environ = append(environ, models.EnvRepoIsWiki+"=true")
} else {
environ = append(environ, models.EnvRepoIsWiki+"=false")
}
}
if !repoExist {
if !receivePack {
ctx.HandleText(http.StatusNotFound, "Repository not found")
return
}
if isWiki { // you cannot send wiki operation before create the repository
ctx.HandleText(http.StatusNotFound, "Repository not found")
return
}
if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg {
ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for organizations.")
return
}
if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser {
ctx.HandleText(http.StatusForbidden, "Push to create is not enabled for users.")
return
}
// Return dummy payload if GET receive-pack
if ctx.Req.Method == http.MethodGet {
dummyInfoRefs(ctx)
return
}
repo, err = repo_service.PushCreateRepo(ctx.User, owner, reponame)
if err != nil {
log.Error("pushCreateRepo: %v", err)
ctx.Status(http.StatusNotFound)
return
}
}
if isWiki {
// Ensure the wiki is enabled before we allow access to it
if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil {
if models.IsErrUnitTypeNotExist(err) {
ctx.HandleText(http.StatusForbidden, "repository wiki is disabled")
return
}
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
return
}
}
environ = append(environ, models.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
w := ctx.Resp
r := ctx.Req
cfg := &serviceConfig{
UploadPack: true,
ReceivePack: true,
Env: environ,
}
r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
dir := models.RepoPath(username, reponame)
if isWiki {
dir = models.RepoPath(username, wikiRepoName)
}
return &serviceHandler{cfg, w, r, dir, cfg.Env}
}
var (
infoRefsCache []byte
infoRefsOnce sync.Once
)
func dummyInfoRefs(ctx *context.Context) {
infoRefsOnce.Do(func() {
tmpDir, err := ioutil.TempDir(os.TempDir(), "gitea-info-refs-cache")
if err != nil {
log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
return
}
defer func() {
if err := util.RemoveAll(tmpDir); err != nil {
log.Error("RemoveAll: %v", err)
}
}()
if err := git.InitRepository(tmpDir, true); err != nil {
log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
return
}
refs, err := git.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunInDirBytes(tmpDir)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
log.Debug("populating infoRefsCache: \n%s", string(refs))
infoRefsCache = refs
})
ctx.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
ctx.Header().Set("Pragma", "no-cache")
ctx.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
ctx.Header().Set("Content-Type", "application/x-git-receive-pack-advertisement")
_, _ = ctx.Write(packetWrite("# service=git-receive-pack\n"))
_, _ = ctx.Write([]byte("0000"))
_, _ = ctx.Write(infoRefsCache)
}
type serviceConfig struct {
UploadPack bool
ReceivePack bool
Env []string
}
type serviceHandler struct {
cfg *serviceConfig
w http.ResponseWriter
r *http.Request
dir string
environ []string
}
func (h *serviceHandler) setHeaderNoCache() {
h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
h.w.Header().Set("Pragma", "no-cache")
h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
}
func (h *serviceHandler) setHeaderCacheForever() {
now := time.Now().Unix()
expires := now + 31536000
h.w.Header().Set("Date", fmt.Sprintf("%d", now))
h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
h.w.Header().Set("Cache-Control", "public, max-age=31536000")
}
func (h *serviceHandler) sendFile(contentType, file string) {
reqFile := path.Join(h.dir, file)
fi, err := os.Stat(reqFile)
if os.IsNotExist(err) {
h.w.WriteHeader(http.StatusNotFound)
return
}
h.w.Header().Set("Content-Type", contentType)
h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
http.ServeFile(h.w, h.r, reqFile)
}
// one or more key=value pairs separated by colons
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
func getGitConfig(option, dir string) string {
out, err := git.NewCommand("config", option).RunInDir(dir)
if err != nil {
log.Error("%v - %s", err, out)
}
return out[0 : len(out)-1]
}
func getConfigSetting(service, dir string) bool {
service = strings.ReplaceAll(service, "-", "")
setting := getGitConfig("http."+service, dir)
if service == "uploadpack" {
return setting != "false"
}
return setting == "true"
}
func hasAccess(service string, h serviceHandler, checkContentType bool) bool {
if checkContentType {
if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) {
return false
}
}
if !(service == "upload-pack" || service == "receive-pack") {
return false
}
if service == "receive-pack" {
return h.cfg.ReceivePack
}
if service == "upload-pack" {
return h.cfg.UploadPack
}
return getConfigSetting(service, h.dir)
}
func serviceRPC(h serviceHandler, service string) {
defer func() {
if err := h.r.Body.Close(); err != nil {
log.Error("serviceRPC: Close: %v", err)
}
}()
if !hasAccess(service, h, true) {
h.w.WriteHeader(http.StatusUnauthorized)
return
}
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
var err error
var reqBody = h.r.Body
// Handle GZIP.
if h.r.Header.Get("Content-Encoding") == "gzip" {
reqBody, err = gzip.NewReader(reqBody)
if err != nil {
log.Error("Fail to create gzip reader: %v", err)
h.w.WriteHeader(http.StatusInternalServerError)
return
}
}
// set this for allow pre-receive and post-receive execute
h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
ctx, cancel := gocontext.WithCancel(git.DefaultContext)
defer cancel()
var stderr bytes.Buffer
cmd := exec.CommandContext(ctx, git.GitExecutable, service, "--stateless-rpc", h.dir)
cmd.Dir = h.dir
cmd.Env = append(os.Environ(), h.environ...)
cmd.Stdout = h.w
cmd.Stdin = reqBody
cmd.Stderr = &stderr
pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir), cancel)
defer process.GetManager().Remove(pid)
if err := cmd.Run(); err != nil {
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
return
}
}
// ServiceUploadPack implements Git Smart HTTP protocol
func ServiceUploadPack(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
serviceRPC(*h, "upload-pack")
}
}
// ServiceReceivePack implements Git Smart HTTP protocol
func ServiceReceivePack(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
serviceRPC(*h, "receive-pack")
}
}
func getServiceType(r *http.Request) string {
serviceType := r.FormValue("service")
if !strings.HasPrefix(serviceType, "git-") {
return ""
}
return strings.Replace(serviceType, "git-", "", 1)
}
func updateServerInfo(dir string) []byte {
out, err := git.NewCommand("update-server-info").RunInDirBytes(dir)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
}
return out
}
func packetWrite(str string) []byte {
s := strconv.FormatInt(int64(len(str)+4), 16)
if len(s)%4 != 0 {
s = strings.Repeat("0", 4-len(s)%4) + s
}
return []byte(s + str)
}
// GetInfoRefs implements Git dumb HTTP
func GetInfoRefs(ctx *context.Context) {
h := httpBase(ctx)
if h == nil {
return
}
h.setHeaderNoCache()
if hasAccess(getServiceType(h.r), *h, false) {
service := getServiceType(h.r)
if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
h.environ = append(os.Environ(), h.environ...)
refs, err := git.NewCommand(service, "--stateless-rpc", "--advertise-refs", ".").RunInDirTimeoutEnv(h.environ, -1, h.dir)
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
h.w.WriteHeader(http.StatusOK)
_, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
_, _ = h.w.Write([]byte("0000"))
_, _ = h.w.Write(refs)
} else {
updateServerInfo(h.dir)
h.sendFile("text/plain; charset=utf-8", "info/refs")
}
}
// GetTextFile implements Git dumb HTTP
func GetTextFile(p string) func(*context.Context) {
return func(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
h.setHeaderNoCache()
file := ctx.Params("file")
if file != "" {
h.sendFile("text/plain", "objects/info/"+file)
} else {
h.sendFile("text/plain", p)
}
}
}
}
// GetInfoPacks implements Git dumb HTTP
func GetInfoPacks(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
h.setHeaderCacheForever()
h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
}
}
// GetLooseObject implements Git dumb HTTP
func GetLooseObject(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
h.setHeaderCacheForever()
h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
ctx.Params("head"), ctx.Params("hash")))
}
}
// GetPackFile implements Git dumb HTTP
func GetPackFile(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
h.setHeaderCacheForever()
h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
}
}
// GetIdxFile implements Git dumb HTTP
func GetIdxFile(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
h.setHeaderCacheForever()
h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
}
}

2599
routers/web/repo/issue.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
// 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 repo
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
// AddDependency adds new dependencies
func AddDependency(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
}
// Check if the Repo is allowed to have dependencies
if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
return
}
depID := ctx.QueryInt64("newDependency")
if err = issue.LoadRepo(); err != nil {
ctx.ServerError("LoadRepo", err)
return
}
// Redirect
defer ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
// Dependency
dep, err := models.GetIssueByID(depID)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist"))
return
}
// Check if both issues are in the same repo if cross repository dependencies is not enabled
if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
return
}
// Check if issue and dependency is the same
if dep.ID == issue.ID {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_same_issue"))
return
}
err = models.CreateIssueDependency(ctx.User, issue, dep)
if err != nil {
if models.IsErrDependencyExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists"))
return
} else if models.IsErrCircularDependency(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular"))
return
} else {
ctx.ServerError("CreateOrUpdateIssueDependency", err)
return
}
}
}
// RemoveDependency removes the dependency
func RemoveDependency(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
}
// Check if the Repo is allowed to have dependencies
if !ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull) {
ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
return
}
depID := ctx.QueryInt64("removeDependencyID")
if err = issue.LoadRepo(); err != nil {
ctx.ServerError("LoadRepo", err)
return
}
// Dependency Type
depTypeStr := ctx.Req.PostForm.Get("dependencyType")
var depType models.DependencyType
switch depTypeStr {
case "blockedBy":
depType = models.DependencyTypeBlockedBy
case "blocking":
depType = models.DependencyTypeBlocking
default:
ctx.Error(http.StatusBadRequest, "GetDependecyType")
return
}
// Dependency
dep, err := models.GetIssueByID(depID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if err = models.RemoveIssueDependency(ctx.User, issue, dep, depType); err != nil {
if models.IsErrDependencyNotExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist"))
return
}
ctx.ServerError("RemoveIssueDependency", err)
return
}
// Redirect
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}

View File

@@ -0,0 +1,222 @@
// Copyright 2017 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 repo
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
)
const (
tplLabels base.TplName = "repo/issue/labels"
)
// Labels render issue's labels page
func Labels(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsLabels"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["LabelTemplates"] = models.LabelTemplates
ctx.HTML(http.StatusOK, tplLabels)
}
// InitializeLabels init labels for a repository
func InitializeLabels(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.InitializeLabelsForm)
if ctx.HasError() {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
return
}
if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName, false); err != nil {
if models.IsErrIssueLabelTemplateLoad(err) {
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
return
}
ctx.ServerError("InitializeLabels", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
// RetrieveLabels find all the labels of a repository and organization
func RetrieveLabels(ctx *context.Context) {
labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
}
for _, l := range labels {
l.CalOpenIssues()
}
ctx.Data["Labels"] = labels
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
for _, l := range orgLabels {
l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
}
ctx.Data["OrgLabels"] = orgLabels
org, err := models.GetOrgByName(ctx.Repo.Owner.LowerName)
if err != nil {
ctx.ServerError("GetOrgByName", err)
return
}
if ctx.User != nil {
ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
if err != nil {
ctx.ServerError("org.IsOwnedBy", err)
return
}
ctx.Org.OrgLink = org.OrganisationLink()
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
ctx.Data["OrganizationLink"] = ctx.Org.OrgLink
}
}
ctx.Data["NumLabels"] = len(labels)
ctx.Data["SortType"] = ctx.Query("sort")
}
// NewLabel create new label for repository
func NewLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsLabels"] = true
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
return
}
l := &models.Label{
RepoID: ctx.Repo.Repository.ID,
Name: form.Title,
Description: form.Description,
Color: form.Color,
}
if err := models.NewLabel(l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
if err != nil {
switch {
case models.IsErrRepoLabelNotExist(err):
ctx.Error(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
}
return
}
l.Name = form.Title
l.Description = form.Description
l.Color = form.Color
if err := models.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
// DeleteLabel delete a label
func DeleteLabel(ctx *context.Context) {
if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteLabel: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/labels",
})
}
// UpdateIssueLabel change issue's labels
func UpdateIssueLabel(ctx *context.Context) {
issues := getActionIssues(ctx)
if ctx.Written() {
return
}
switch action := ctx.Query("action"); action {
case "clear":
for _, issue := range issues {
if err := issue_service.ClearLabels(issue, ctx.User); err != nil {
ctx.ServerError("ClearLabels", err)
return
}
}
case "attach", "detach", "toggle":
label, err := models.GetLabelByID(ctx.QueryInt64("id"))
if err != nil {
if models.IsErrRepoLabelNotExist(err) {
ctx.Error(http.StatusNotFound, "GetLabelByID")
} else {
ctx.ServerError("GetLabelByID", err)
}
return
}
if action == "toggle" {
// detach if any issues already have label, otherwise attach
action = "attach"
for _, issue := range issues {
if issue.HasLabel(label.ID) {
action = "detach"
break
}
}
}
if action == "attach" {
for _, issue := range issues {
if err = issue_service.AddLabel(issue, ctx.User, label); err != nil {
ctx.ServerError("AddLabel", err)
return
}
}
} else {
for _, issue := range issues {
if err = issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
ctx.ServerError("RemoveLabel", err)
return
}
}
}
default:
log.Warn("Unrecognized action: %s", action)
ctx.Error(http.StatusInternalServerError)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}

View File

@@ -0,0 +1,168 @@
// Copyright 2017 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 repo
import (
"net/http"
"strconv"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
)
func int64SliceToCommaSeparated(a []int64) string {
s := ""
for i, n := range a {
if i > 0 {
s += ","
}
s += strconv.Itoa(int(n))
}
return s
}
func TestInitializeLabels(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/labels/initialize")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.Label{
RepoID: 2,
Name: "enhancement",
Color: "#84b6eb",
})
assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
}
func TestRetrieveLabels(t *testing.T) {
models.PrepareTestEnv(t)
for _, testCase := range []struct {
RepoID int64
Sort string
ExpectedLabelIDs []int64
}{
{1, "", []int64{1, 2}},
{1, "leastissues", []int64{2, 1}},
{2, "", []int64{}},
} {
ctx := test.MockContext(t, "user/repo/issues")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, testCase.RepoID)
ctx.Req.Form.Set("sort", testCase.Sort)
RetrieveLabels(ctx)
assert.False(t, ctx.Written())
labels, ok := ctx.Data["Labels"].([]*models.Label)
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
}
}
}
}
func TestNewLabel(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/labels/edit")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
Title: "newlabel",
Color: "#abcdef",
})
NewLabel(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.Label{
Name: "newlabel",
Color: "#abcdef",
})
assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
}
func TestUpdateLabel(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/labels/edit")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
ID: 2,
Title: "newnameforlabel",
Color: "#abcdef",
})
UpdateLabel(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.Label{
ID: 2,
Name: "newnameforlabel",
Color: "#abcdef",
})
assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
}
func TestDeleteLabel(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/labels/delete")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
models.AssertNotExistsBean(t, &models.Label{ID: 2})
models.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2})
assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
func TestUpdateIssueLabel_Clear(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1})
models.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3})
models.CheckConsistencyFor(t, &models.Label{})
}
func TestUpdateIssueLabel_Toggle(t *testing.T) {
for _, testCase := range []struct {
Action string
IssueIDs []int64
LabelID int64
ExpectedAdd bool // whether we expect the label to be added to the issues
}{
{"attach", []int64{1, 3}, 1, true},
{"detach", []int64{1, 3}, 1, false},
{"toggle", []int64{1, 3}, 1, false},
{"toggle", []int64{1, 2}, 2, true},
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
for _, issueID := range testCase.IssueIDs {
models.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{
IssueID: issueID,
LabelID: testCase.LabelID,
})
}
models.CheckConsistencyFor(t, &models.Label{})
}
}

View File

@@ -0,0 +1,72 @@
// Copyright 2019 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 repo
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
// LockIssue locks an issue. This would limit commenting abilities to
// users with write access to the repo.
func LockIssue(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.IssueLockForm)
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
ctx.Redirect(issue.HTMLURL())
return
}
if !form.HasValidReason() {
ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
ctx.Redirect(issue.HTMLURL())
return
}
if err := models.LockIssue(&models.IssueLockOptions{
Doer: ctx.User,
Issue: issue,
Reason: form.Reason,
}); err != nil {
ctx.ServerError("LockIssue", err)
return
}
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}
// UnlockIssue unlocks a previously locked issue.
func UnlockIssue(ctx *context.Context) {
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if !issue.IsLocked {
ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
ctx.Redirect(issue.HTMLURL())
return
}
if err := models.UnlockIssue(&models.IssueLockOptions{
Doer: ctx.User,
Issue: issue,
}); err != nil {
ctx.ServerError("UnlockIssue", err)
return
}
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}

View File

@@ -0,0 +1,108 @@
// Copyright 2017 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 repo
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
)
// IssueStopwatch creates or stops a stopwatch for the given issue.
func IssueStopwatch(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
var showSuccessMessage bool
if !models.StopwatchExists(c.User.ID, issue.ID) {
showSuccessMessage = true
}
if !c.Repo.CanUseTimetracker(issue, c.User) {
c.NotFound("CanUseTimetracker", nil)
return
}
if err := models.CreateOrStopIssueStopwatch(c.User, issue); err != nil {
c.ServerError("CreateOrStopIssueStopwatch", err)
return
}
if showSuccessMessage {
c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
}
url := issue.HTMLURL()
c.Redirect(url, http.StatusSeeOther)
}
// CancelStopwatch cancel the stopwatch
func CancelStopwatch(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
if !c.Repo.CanUseTimetracker(issue, c.User) {
c.NotFound("CanUseTimetracker", nil)
return
}
if err := models.CancelStopwatch(c.User, issue); err != nil {
c.ServerError("CancelStopwatch", err)
return
}
url := issue.HTMLURL()
c.Redirect(url, http.StatusSeeOther)
}
// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
func GetActiveStopwatch(c *context.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/api") {
return
}
if !c.IsSigned {
return
}
_, sw, err := models.HasUserStopwatch(c.User.ID)
if err != nil {
c.ServerError("HasUserStopwatch", err)
return
}
if sw == nil || sw.ID == 0 {
return
}
issue, err := models.GetIssueByID(sw.IssueID)
if err != nil || issue == nil {
c.ServerError("GetIssueByID", err)
return
}
if err = issue.LoadRepo(); err != nil {
c.ServerError("LoadRepo", err)
return
}
c.Data["ActiveStopwatch"] = StopwatchTmplInfo{
issue.Repo.FullName(),
issue.Index,
sw.Seconds() + 1, // ensure time is never zero in ui
}
}
// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
type StopwatchTmplInfo struct {
RepoSlug string
IssueIndex int64
Seconds int64
}

View File

@@ -0,0 +1,324 @@
// Copyright 2020 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 repo
import (
"testing"
"code.gitea.io/gitea/models"
"github.com/stretchr/testify/assert"
)
func TestCombineLabelComments(t *testing.T) {
var kases = []struct {
name string
beforeCombined []*models.Comment
afterCombined []*models.Comment
}{
{
name: "kase 1",
beforeCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
afterCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
AddedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
RemovedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
},
{
name: "kase 2",
beforeCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 70,
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
afterCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
AddedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "",
CreatedUnix: 70,
RemovedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
},
{
name: "kase 3",
beforeCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 2,
Content: "",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
afterCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
AddedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
{
Type: models.CommentTypeLabel,
PosterID: 2,
Content: "",
CreatedUnix: 0,
RemovedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
{
Type: models.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
},
{
name: "kase 4",
beforeCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/backport",
},
CreatedUnix: 10,
},
},
afterCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 10,
AddedLabels: []*models.Label{
{
Name: "kind/bug",
},
{
Name: "kind/backport",
},
},
Label: &models.Label{
Name: "kind/bug",
},
},
},
},
{
name: "kase 5",
beforeCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeComment,
PosterID: 2,
Content: "testtest",
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "",
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
},
afterCombined: []*models.Comment{
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "1",
Label: &models.Label{
Name: "kind/bug",
},
AddedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
CreatedUnix: 0,
},
{
Type: models.CommentTypeComment,
PosterID: 2,
Content: "testtest",
CreatedUnix: 0,
},
{
Type: models.CommentTypeLabel,
PosterID: 1,
Content: "",
RemovedLabels: []*models.Label{
{
Name: "kind/bug",
},
},
Label: &models.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
},
},
}
for _, kase := range kases {
t.Run(kase.name, func(t *testing.T) {
var issue = models.Issue{
Comments: kase.beforeCombined,
}
combineLabelComments(&issue)
assert.EqualValues(t, kase.afterCombined, issue.Comments)
})
}
}

View File

@@ -0,0 +1,86 @@
// Copyright 2017 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 repo
import (
"net/http"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
// AddTimeManually tracks time manually
func AddTimeManually(c *context.Context) {
form := web.GetForm(c).(*forms.AddTimeManuallyForm)
issue := GetActionIssue(c)
if c.Written() {
return
}
if !c.Repo.CanUseTimetracker(issue, c.User) {
c.NotFound("CanUseTimetracker", nil)
return
}
url := issue.HTMLURL()
if c.HasError() {
c.Flash.Error(c.GetErrMsg())
c.Redirect(url)
return
}
total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
if total <= 0 {
c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small"))
c.Redirect(url, http.StatusSeeOther)
return
}
if _, err := models.AddTime(c.User, issue, int64(total.Seconds()), time.Now()); err != nil {
c.ServerError("AddTime", err)
return
}
c.Redirect(url, http.StatusSeeOther)
}
// DeleteTime deletes tracked time
func DeleteTime(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
if !c.Repo.CanUseTimetracker(issue, c.User) {
c.NotFound("CanUseTimetracker", nil)
return
}
t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
if err != nil {
if models.IsErrNotExist(err) {
c.NotFound("time not found", err)
return
}
c.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err.Error())
return
}
// only OP or admin may delete
if !c.IsSigned || (!c.IsUserSiteAdmin() && c.User.ID != t.UserID) {
c.Error(http.StatusForbidden, "not allowed")
return
}
if err = models.DeleteTime(t); err != nil {
c.ServerError("DeleteTime", err)
return
}
c.Flash.Success(c.Tr("repo.issues.del_time_history", models.SecToTime(t.Time)))
c.Redirect(issue.HTMLURL())
}

View File

@@ -0,0 +1,57 @@
// Copyright 2017 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 repo
import (
"net/http"
"strconv"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
// IssueWatch sets issue watching
func IssueWatch(ctx *context.Context) {
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
if log.IsTrace() {
if ctx.IsSigned {
issueType := "issues"
if issue.IsPull {
issueType = "pulls"
}
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
"User in Repo has Permissions: %-+v",
ctx.User,
log.NewColoredIDValue(issue.PosterID),
issueType,
ctx.Repo.Repository,
ctx.Repo.Permission)
} else {
log.Trace("Permission Denied: Not logged in")
}
}
ctx.Error(http.StatusForbidden)
return
}
watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
if err != nil {
ctx.ServerError("watch is not bool", err)
return
}
if err := models.CreateOrUpdateIssueWatch(ctx.User.ID, issue.ID, watch); err != nil {
ctx.ServerError("CreateOrUpdateIssueWatch", err)
return
}
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}

537
routers/web/repo/lfs.go Normal file
View File

@@ -0,0 +1,537 @@
// Copyright 2019 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 repo
import (
"bytes"
"fmt"
gotemplate "html/template"
"io"
"io/ioutil"
"net/http"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/typesniffer"
)
const (
tplSettingsLFS base.TplName = "repo/settings/lfs"
tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks"
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
)
// LFSFiles shows a repository's LFS files
func LFSFiles(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSFiles", nil)
return
}
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
total, err := ctx.Repo.Repository.CountLFSMetaObjects()
if err != nil {
ctx.ServerError("LFSFiles", err)
return
}
ctx.Data["Total"] = total
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
ctx.Data["PageIsSettingsLFS"] = true
lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
if err != nil {
ctx.ServerError("LFSFiles", err)
return
}
ctx.Data["LFSFiles"] = lfsMetaObjects
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsLFS)
}
// LFSLocks shows a repository's LFS locks
func LFSLocks(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSLocks", nil)
return
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
}
ctx.Data["Total"] = total
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
ctx.Data["PageIsSettingsLFS"] = true
lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
}
ctx.Data["LFSLocks"] = lfsLocks
if len(lfsLocks) == 0 {
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
return
}
// Clone base repo.
tmpBasePath, err := models.CreateTemporaryPath("locks")
if err != nil {
log.Error("Failed to create temporary path: %v", err)
ctx.ServerError("LFSLocks", err)
return
}
defer func() {
if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
}
}()
if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
}); err != nil {
log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
return
}
gitRepo, err := git.OpenRepository(tmpBasePath)
if err != nil {
log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
return
}
defer gitRepo.Close()
filenames := make([]string, len(lfsLocks))
for i, lock := range lfsLocks {
filenames[i] = lock.Path
}
if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
return
}
name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
Attributes: []string{"lockable"},
Filenames: filenames,
CachedOnly: true,
})
if err != nil {
log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", err)
return
}
lockables := make([]bool, len(lfsLocks))
for i, lock := range lfsLocks {
attribute2info, has := name2attribute2info[lock.Path]
if !has {
continue
}
if attribute2info["lockable"] != "set" {
continue
}
lockables[i] = true
}
ctx.Data["Lockables"] = lockables
filelist, err := gitRepo.LsFiles(filenames...)
if err != nil {
log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", err)
return
}
filemap := make(map[string]bool, len(filelist))
for _, name := range filelist {
filemap[name] = true
}
linkable := make([]bool, len(lfsLocks))
for i, lock := range lfsLocks {
linkable[i] = filemap[lock.Path]
}
ctx.Data["Linkable"] = linkable
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsLFSLocks)
}
// LFSLockFile locks a file
func LFSLockFile(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSLocks", nil)
return
}
originalPath := ctx.Query("path")
lockPath := originalPath
if len(lockPath) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
if lockPath[len(lockPath)-1] == '/' {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
lockPath = path.Clean("/" + lockPath)[1:]
if len(lockPath) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
_, err := models.CreateLFSLock(&models.LFSLock{
Repo: ctx.Repo.Repository,
Path: lockPath,
Owner: ctx.User,
})
if err != nil {
if models.IsErrLFSLockAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
ctx.ServerError("LFSLockFile", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
}
// LFSUnlock forcibly unlocks an LFS lock
func LFSUnlock(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSUnlock", nil)
return
}
_, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
if err != nil {
ctx.ServerError("LFSUnlock", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
}
// LFSFileGet serves a single LFS file
func LFSFileGet(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSFileGet", nil)
return
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
oid := ctx.Params("oid")
ctx.Data["Title"] = oid
ctx.Data["PageIsSettingsLFS"] = true
meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
if err != nil {
if err == models.ErrLFSObjectNotExist {
ctx.NotFound("LFSFileGet", nil)
return
}
ctx.ServerError("LFSFileGet", err)
return
}
ctx.Data["LFSFile"] = meta
dataRc, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
ctx.ServerError("LFSFileGet", err)
return
}
defer dataRc.Close()
buf := make([]byte, 1024)
n, err := dataRc.Read(buf)
if err != nil {
ctx.ServerError("Data", err)
return
}
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
ctx.Data["IsTextFile"] = st.IsText()
isRepresentableAsText := st.IsRepresentableAsText()
fileSize := meta.Size
ctx.Data["FileSize"] = meta.Size
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
switch {
case isRepresentableAsText:
if st.IsSvgImage() {
ctx.Data["IsImageFile"] = true
}
if fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
buf := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
// Building code view blocks with line number on server side.
fileContent, _ := ioutil.ReadAll(buf)
var output bytes.Buffer
lines := strings.Split(string(fileContent), "\n")
//Remove blank line at the end of file
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
for index, line := range lines {
line = gotemplate.HTMLEscapeString(line)
if index != len(lines)-1 {
line += "\n"
}
output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
}
ctx.Data["FileContent"] = gotemplate.HTML(output.String())
output.Reset()
for i := 0; i < len(lines); i++ {
output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
case st.IsPDF():
ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
ctx.Data["IsAudioFile"] = true
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
}
ctx.HTML(http.StatusOK, tplSettingsLFSFile)
}
// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
func LFSDelete(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSDelete", nil)
return
}
oid := ctx.Params("oid")
count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid)
if err != nil {
ctx.ServerError("LFSDelete", err)
return
}
// FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
// Please note a similar condition happens in models/repo.go DeleteRepository
if count == 0 {
oidPath := path.Join(oid[0:2], oid[2:4], oid[4:])
err = storage.LFS.Delete(oidPath)
if err != nil {
ctx.ServerError("LFSDelete", err)
return
}
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
}
// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
func LFSFileFind(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSFind", nil)
return
}
oid := ctx.Query("oid")
size := ctx.QueryInt64("size")
if len(oid) == 0 || size == 0 {
ctx.NotFound("LFSFind", nil)
return
}
sha := ctx.Query("sha")
ctx.Data["Title"] = oid
ctx.Data["PageIsSettingsLFS"] = true
var hash git.SHA1
if len(sha) == 0 {
pointer := lfs.Pointer{Oid: oid, Size: size}
hash = git.ComputeBlobHash([]byte(pointer.StringContent()))
sha = hash.String()
} else {
hash = git.MustIDFromString(sha)
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
ctx.Data["Oid"] = oid
ctx.Data["Size"] = size
ctx.Data["SHA"] = sha
results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, hash)
if err != nil && err != io.EOF {
log.Error("Failure in FindLFSFile: %v", err)
ctx.ServerError("LFSFind: FindLFSFile.", err)
return
}
ctx.Data["Results"] = results
ctx.HTML(http.StatusOK, tplSettingsLFSFileFind)
}
// LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
func LFSPointerFiles(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSFileGet", nil)
return
}
ctx.Data["PageIsSettingsLFS"] = true
err := git.LoadGitVersion()
if err != nil {
log.Fatal("Error retrieving git version: %v", err)
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
err = func() error {
pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1)
go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
numPointers := 0
var numAssociated, numNoExist, numAssociatable int
type pointerResult struct {
SHA string
Oid string
Size int64
InRepo bool
Exists bool
Accessible bool
}
results := []pointerResult{}
contentStore := lfs.NewContentStore()
repo := ctx.Repo.Repository
for pointerBlob := range pointerChan {
numPointers++
result := pointerResult{
SHA: pointerBlob.Hash,
Oid: pointerBlob.Oid,
Size: pointerBlob.Size,
}
if _, err := repo.GetLFSMetaObjectByOid(pointerBlob.Oid); err != nil {
if err != models.ErrLFSObjectNotExist {
return err
}
} else {
result.InRepo = true
}
result.Exists, err = contentStore.Exists(pointerBlob.Pointer)
if err != nil {
return err
}
if result.Exists {
if !result.InRepo {
// Can we fix?
// OK well that's "simple"
// - we need to check whether current user has access to a repo that has access to the file
result.Accessible, err = models.LFSObjectAccessible(ctx.User, pointerBlob.Oid)
if err != nil {
return err
}
} else {
result.Accessible = true
}
}
if result.InRepo {
numAssociated++
}
if !result.Exists {
numNoExist++
}
if !result.InRepo && result.Accessible {
numAssociatable++
}
results = append(results, result)
}
err, has := <-errChan
if has {
return err
}
ctx.Data["Pointers"] = results
ctx.Data["NumPointers"] = numPointers
ctx.Data["NumAssociated"] = numAssociated
ctx.Data["NumAssociatable"] = numAssociatable
ctx.Data["NumNoExist"] = numNoExist
ctx.Data["NumNotAssociated"] = numPointers - numAssociated
return nil
}()
if err != nil {
ctx.ServerError("LFSPointerFiles", err)
return
}
ctx.HTML(http.StatusOK, tplSettingsLFSPointers)
}
// LFSAutoAssociate auto associates accessible lfs files
func LFSAutoAssociate(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSAutoAssociate", nil)
return
}
oids := ctx.QueryStrings("oid")
metas := make([]*models.LFSMetaObject, len(oids))
for i, oid := range oids {
idx := strings.IndexRune(oid, ' ')
if idx < 0 || idx+1 > len(oid) {
ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s", oid))
return
}
var err error
metas[i] = &models.LFSMetaObject{}
metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
if err != nil {
ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err))
return
}
metas[i].Oid = oid[:idx]
//metas[i].RepositoryID = ctx.Repo.Repository.ID
}
if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil {
ctx.ServerError("LFSAutoAssociate", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
}

View File

@@ -0,0 +1,16 @@
// Copyright 2017 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 repo
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", "..", ".."))
}

View File

@@ -0,0 +1,72 @@
// Copyright 2020 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 repo
import (
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
)
// SetEditorconfigIfExists set editor config as render variable
func SetEditorconfigIfExists(ctx *context.Context) {
if ctx.Repo.Repository.IsEmpty {
ctx.Data["Editorconfig"] = nil
return
}
ec, err := ctx.Repo.GetEditorconfig()
if err != nil && !git.IsErrNotExist(err) {
description := fmt.Sprintf("Error while getting .editorconfig file: %v", err)
if err := models.CreateRepositoryNotice(description); err != nil {
ctx.ServerError("ErrCreatingReporitoryNotice", err)
}
return
}
ctx.Data["Editorconfig"] = ec
}
// SetDiffViewStyle set diff style as render variable
func SetDiffViewStyle(ctx *context.Context) {
queryStyle := ctx.Query("style")
if !ctx.IsSigned {
ctx.Data["IsSplitStyle"] = queryStyle == "split"
return
}
var (
userStyle = ctx.User.DiffViewStyle
style string
)
if queryStyle == "unified" || queryStyle == "split" {
style = queryStyle
} else if userStyle == "unified" || userStyle == "split" {
style = userStyle
} else {
style = "unified"
}
ctx.Data["IsSplitStyle"] = style == "split"
if err := ctx.User.UpdateDiffViewStyle(style); err != nil {
ctx.ServerError("ErrUpdateDiffViewStyle", err)
}
}
// SetWhitespaceBehavior set whitespace behavior as render variable
func SetWhitespaceBehavior(ctx *context.Context) {
whitespaceBehavior := ctx.Query("whitespace")
switch whitespaceBehavior {
case "ignore-all", "ignore-eol", "ignore-change":
ctx.Data["WhitespaceBehavior"] = whitespaceBehavior
default:
ctx.Data["WhitespaceBehavior"] = ""
}
}

254
routers/web/repo/migrate.go Normal file
View File

@@ -0,0 +1,254 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 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 repo
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/migrations"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
const (
tplMigrate base.TplName = "repo/migrate/migrate"
)
// Migrate render migration of repository page
func Migrate(ctx *context.Context) {
if setting.Repository.DisableMigrations {
ctx.Error(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
return
}
serviceType := structs.GitServiceType(ctx.QueryInt("service_type"))
setMigrationContextData(ctx, serviceType)
if serviceType == 0 {
ctx.Data["Org"] = ctx.Query("org")
ctx.Data["Mirror"] = ctx.Query("mirror")
ctx.HTML(http.StatusOK, tplMigrate)
return
}
ctx.Data["private"] = getRepoPrivate(ctx)
ctx.Data["mirror"] = ctx.Query("mirror") == "1"
ctx.Data["lfs"] = ctx.Query("lfs") == "1"
ctx.Data["wiki"] = ctx.Query("wiki") == "1"
ctx.Data["milestones"] = ctx.Query("milestones") == "1"
ctx.Data["labels"] = ctx.Query("labels") == "1"
ctx.Data["issues"] = ctx.Query("issues") == "1"
ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1"
ctx.Data["releases"] = ctx.Query("releases") == "1"
ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
ctx.HTML(http.StatusOK, base.TplName("repo/migrate/"+serviceType.Name()))
}
func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *forms.MigrateRepoForm) {
if setting.Repository.DisableMigrations {
ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
return
}
switch {
case migrations.IsRateLimitError(err):
ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case models.IsErrReachLimitOfRepo(err):
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
case models.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
case models.IsErrRepoFilesAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
switch {
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
default:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
}
case models.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
remoteAddr, _ := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.Data["Err_Auth"] = true
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
} else if strings.Contains(err.Error(), "fatal:") {
ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
} else {
ctx.ServerError(name, err)
}
}
}
func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplName, form *forms.MigrateRepoForm) {
if models.IsErrInvalidCloneAddr(err) {
addrErr := err.(*models.ErrInvalidCloneAddr)
switch {
case addrErr.IsProtocolInvalid:
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
} else if len(addrErr.PrivateNet) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tpl, form)
}
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
default:
log.Error("Error whilst updating url: %v", err)
ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
}
} else {
log.Error("Error whilst updating url: %v", err)
ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
}
}
// MigratePost response for migrating from external git repository
func MigratePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.MigrateRepoForm)
if setting.Repository.DisableMigrations {
ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
return
}
serviceType := structs.GitServiceType(form.Service)
setMigrationContextData(ctx, serviceType)
ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
tpl := base.TplName("repo/migrate/" + serviceType.Name())
if ctx.HasError() {
ctx.HTML(http.StatusOK, tpl)
return
}
remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.User)
}
if err != nil {
ctx.Data["Err_CloneAddr"] = true
handleMigrateRemoteAddrError(ctx, err, tpl, form)
return
}
form.LFS = form.LFS && setting.LFS.StartServer
if form.LFS && len(form.LFSEndpoint) > 0 {
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
if ep == nil {
ctx.Data["Err_LFSEndpoint"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
return
}
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
if err != nil {
ctx.Data["Err_LFSEndpoint"] = true
handleMigrateRemoteAddrError(ctx, err, tpl, form)
return
}
}
var opts = migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
GitServiceType: serviceType,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror && !setting.Repository.DisableMirrors,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
AuthToken: form.AuthToken,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: form.Issues || form.PullRequests,
PullRequests: form.PullRequests,
Releases: form.Releases,
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}
err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName, false)
if err != nil {
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
return
}
err = task.MigrateRepository(ctx.User, ctxUser, opts)
if err == nil {
ctx.Redirect(ctxUser.HomeLink() + "/" + opts.RepoName)
return
}
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
}
func setMigrationContextData(ctx *context.Context, serviceType structs.GitServiceType) {
ctx.Data["Title"] = ctx.Tr("new_migrate")
ctx.Data["LFSActive"] = setting.LFS.StartServer
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors
// Plain git should be first
ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...)
ctx.Data["service"] = serviceType
}

View File

@@ -0,0 +1,299 @@
// 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 repo
import (
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"xorm.io/builder"
)
const (
tplMilestone base.TplName = "repo/issue/milestones"
tplMilestoneNew base.TplName = "repo/issue/milestone_new"
tplMilestoneIssues base.TplName = "repo/issue/milestone_issues"
)
// Milestones render milestones page
func Milestones(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.milestones")
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsMilestones"] = true
isShowClosed := ctx.Query("state") == "closed"
stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID}))
if err != nil {
ctx.ServerError("MilestoneStats", err)
return
}
ctx.Data["OpenCount"] = stats.OpenCount
ctx.Data["ClosedCount"] = stats.ClosedCount
sortType := ctx.Query("sort")
keyword := strings.Trim(ctx.Query("q"), " ")
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
var total int
var state structs.StateType
if !isShowClosed {
total = int(stats.OpenCount)
state = structs.StateOpen
} else {
total = int(stats.ClosedCount)
state = structs.StateClosed
}
miles, err := models.GetMilestones(models.GetMilestonesOption{
ListOptions: models.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
},
RepoID: ctx.Repo.Repository.ID,
State: state,
SortType: sortType,
Name: keyword,
})
if err != nil {
ctx.ServerError("GetMilestones", err)
return
}
if ctx.Repo.Repository.IsTimetrackerEnabled() {
if err := miles.LoadTotalTrackedTimes(); err != nil {
ctx.ServerError("LoadTotalTrackedTimes", err)
return
}
}
for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, m.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
}
ctx.Data["Milestones"] = miles
if isShowClosed {
ctx.Data["State"] = "closed"
} else {
ctx.Data["State"] = "open"
}
ctx.Data["SortType"] = sortType
ctx.Data["Keyword"] = keyword
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "state", "State")
pager.AddParam(ctx, "q", "Keyword")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestone)
}
// NewMilestone render creating milestone page
func NewMilestone(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsMilestones"] = true
ctx.HTML(http.StatusOK, tplMilestoneNew)
}
// NewMilestonePost response for creating milestone
func NewMilestonePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsMilestones"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplMilestoneNew)
return
}
if len(form.Deadline) == 0 {
form.Deadline = "9999-12-31"
}
deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
if err != nil {
ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return
}
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
if err = models.NewMilestone(&models.Milestone{
RepoID: ctx.Repo.Repository.ID,
Name: form.Title,
Content: form.Content,
DeadlineUnix: timeutil.TimeStamp(deadline.Unix()),
}); err != nil {
ctx.ServerError("NewMilestone", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
}
// EditMilestone render edting milestone page
func EditMilestone(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
ctx.Data["PageIsMilestones"] = true
ctx.Data["PageIsEditMilestone"] = true
m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrMilestoneNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetMilestoneByRepoID", err)
}
return
}
ctx.Data["title"] = m.Name
ctx.Data["content"] = m.Content
if len(m.DeadlineString) > 0 {
ctx.Data["deadline"] = m.DeadlineString
}
ctx.HTML(http.StatusOK, tplMilestoneNew)
}
// EditMilestonePost response for edting milestone
func EditMilestonePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateMilestoneForm)
ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
ctx.Data["PageIsMilestones"] = true
ctx.Data["PageIsEditMilestone"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplMilestoneNew)
return
}
if len(form.Deadline) == 0 {
form.Deadline = "9999-12-31"
}
deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
if err != nil {
ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return
}
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
m, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrMilestoneNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetMilestoneByRepoID", err)
}
return
}
m.Name = form.Title
m.Content = form.Content
m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix())
if err = models.UpdateMilestone(m, m.IsClosed); err != nil {
ctx.ServerError("UpdateMilestone", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
}
// ChangeMilestoneStatus response for change a milestone's status
func ChangeMilestoneStatus(ctx *context.Context) {
toClose := false
switch ctx.Params(":action") {
case "open":
toClose = false
case "close":
toClose = true
default:
ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
}
id := ctx.ParamsInt64(":id")
if err := models.ChangeMilestoneStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
if models.IsErrMilestoneNotExist(err) {
ctx.NotFound("", err)
} else {
ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
}
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=" + ctx.Params(":action"))
}
// DeleteMilestone delete a milestone
func DeleteMilestone(ctx *context.Context) {
if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
ctx.Flash.Error("DeleteMilestoneByRepoID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/milestones",
})
}
// MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone
func MilestoneIssuesAndPulls(ctx *context.Context) {
milestoneID := ctx.ParamsInt64(":id")
milestone, err := models.GetMilestoneByID(milestoneID)
if err != nil {
if models.IsErrMilestoneNotExist(err) {
ctx.NotFound("GetMilestoneByID", err)
return
}
ctx.ServerError("GetMilestoneByID", err)
return
}
milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, milestone.Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone
issues(ctx, milestoneID, 0, util.OptionalBoolNone)
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
ctx.HTML(http.StatusOK, tplMilestoneIssues)
}

View File

@@ -0,0 +1,665 @@
// Copyright 2020 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 repo
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
const (
tplProjects base.TplName = "repo/projects/list"
tplProjectsNew base.TplName = "repo/projects/new"
tplProjectsView base.TplName = "repo/projects/view"
tplGenericProjectsNew base.TplName = "user/project"
)
// MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) {
if models.UnitTypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil)
return
}
if ctx.Repo.Repository != nil {
if !ctx.Repo.CanRead(models.UnitTypeProjects) {
ctx.NotFound("MustEnableProjects", nil)
return
}
}
}
// Projects renders the home page of projects
func Projects(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.project_board")
sortType := ctx.QueryTrim("sort")
isShowClosed := strings.ToLower(ctx.QueryTrim("state")) == "closed"
repo := ctx.Repo.Repository
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
var total int
if !isShowClosed {
total = repo.NumOpenProjects
} else {
total = repo.NumClosedProjects
}
projects, count, err := models.GetProjects(models.ProjectSearchOptions{
RepoID: repo.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
SortType: sortType,
Type: models.ProjectTypeRepository,
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
}
for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, projects[i].Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
}
ctx.Data["Projects"] = projects
if isShowClosed {
ctx.Data["State"] = "closed"
} else {
ctx.Data["State"] = "open"
}
numPages := 0
if count > 0 {
numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["IsProjectsPage"] = true
ctx.Data["SortType"] = sortType
ctx.HTML(http.StatusOK, tplProjects)
}
// NewProject render creating a project page
func NewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
// NewProjectPost creates a new project
func NewProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.HTML(http.StatusOK, tplProjectsNew)
return
}
if err := models.NewProject(&models.Project{
RepoID: ctx.Repo.Repository.ID,
Title: form.Title,
Description: form.Content,
CreatorID: ctx.User.ID,
BoardType: form.BoardType,
Type: models.ProjectTypeRepository,
}); err != nil {
ctx.ServerError("NewProject", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
// ChangeProjectStatus updates the status of a project between "open" and "close"
func ChangeProjectStatus(ctx *context.Context) {
toClose := false
switch ctx.Params(":action") {
case "open":
toClose = false
case "close":
toClose = true
default:
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
id := ctx.ParamsInt64(":id")
if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", err)
} else {
ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
}
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action"))
}
// DeleteProject delete a project
func DeleteProject(ctx *context.Context) {
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
if err := models.DeleteProjectByID(p.ID); err != nil {
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/projects",
})
}
// EditProject allows a project to be edited
func EditProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsProjects"] = true
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
ctx.Data["title"] = p.Title
ctx.Data["content"] = p.Description
ctx.HTML(http.StatusOK, tplProjectsNew)
}
// EditProjectPost response for editing a project
func EditProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsProjects"] = true
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplProjectsNew)
return
}
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
p.Title = form.Title
p.Description = form.Content
if err = models.UpdateProject(p); err != nil {
ctx.ServerError("UpdateProjects", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
// ViewProject renders the project board for a project
func ViewProject(ctx *context.Context) {
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
boards, err := models.GetProjectBoards(project.ID)
if err != nil {
ctx.ServerError("GetProjectBoards", err)
return
}
if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
}
issueList, err := boards.LoadIssues()
if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err)
return
}
ctx.Data["Issues"] = issueList
linkedPrsMap := make(map[int64][]*models.Issue)
for _, issue := range issueList {
var referencedIds []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIds = append(referencedIds, comment.RefIssueID)
}
}
if len(referencedIds) > 0 {
if linkedPrs, err := models.Issues(&models.IssuesOptions{
IssueIDs: referencedIds,
IsPull: util.OptionalBoolTrue,
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
}
}
ctx.Data["LinkedPRs"] = linkedPrsMap
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, project.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["Project"] = project
ctx.Data["Boards"] = boards
ctx.Data["PageIsProjects"] = true
ctx.Data["RequiresDraggable"] = true
ctx.HTML(http.StatusOK, tplProjectsView)
}
// UpdateIssueProject change an issue's project
func UpdateIssueProject(ctx *context.Context) {
issues := getActionIssues(ctx)
if ctx.Written() {
return
}
projectID := ctx.QueryInt64("id")
for _, issue := range issues {
oldProjectID := issue.ProjectID()
if oldProjectID == projectID {
continue
}
if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// DeleteProjectBoard allows for the deletion of a project board
func DeleteProjectBoard(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return
}
if pb.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
})
return
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
})
return
}
if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
ctx.ServerError("DeleteProjectBoardByID", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// AddBoardToProjectPost allows a new board to be added to a project.
func AddBoardToProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if err := models.NewProjectBoard(&models.ProjectBoard{
ProjectID: project.ID,
Title: form.Title,
CreatorID: ctx.User.ID,
}); err != nil {
ctx.ServerError("NewProjectBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return nil, nil
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return nil, nil
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return nil, nil
}
board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return nil, nil
}
if board.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
})
return nil, nil
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
})
return nil, nil
}
return project, board
}
// EditProjectBoard allows a project board's to be updated
func EditProjectBoard(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
_, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if form.Title != "" {
board.Title = form.Title
}
if form.Sorting != 0 {
board.Sorting = form.Sorting
}
if err := models.UpdateProjectBoard(board); err != nil {
ctx.ServerError("UpdateProjectBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
func SetDefaultProjectBoard(ctx *context.Context) {
project, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
ctx.ServerError("SetDefaultBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// MoveIssueAcrossBoards move a card from one board to another in a project
func MoveIssueAcrossBoards(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
var board *models.ProjectBoard
if ctx.ParamsInt64(":boardID") == 0 {
board = &models.ProjectBoard{
ID: 0,
ProjectID: 0,
Title: ctx.Tr("repo.projects.type.uncategorized"),
}
} else {
board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
if models.IsErrProjectBoardNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectBoard", err)
}
return
}
if board.ProjectID != p.ID {
ctx.NotFound("", nil)
return
}
}
issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
return
}
if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
ctx.ServerError("MoveIssueAcrossProjectBoards", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// CreateProject renders the generic project creation page
func CreateProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplGenericProjectsNew)
}
// CreateProjectPost creates an individual and/or organization project
func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
user := checkContextUser(ctx, form.UID)
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = user
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplGenericProjectsNew)
return
}
var projectType = models.ProjectTypeIndividual
if user.IsOrganization() {
projectType = models.ProjectTypeOrganization
}
if err := models.NewProject(&models.Project{
Title: form.Title,
Description: form.Content,
CreatorID: user.ID,
BoardType: form.BoardType,
Type: projectType,
}); err != nil {
ctx.ServerError("NewProject", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/")
}

View File

@@ -0,0 +1,28 @@
// Copyright 2020 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 repo
import (
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestCheckProjectBoardChangePermissions(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/projects/1/2")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.SetParams(":id", "1")
ctx.SetParams(":boardID", "2")
project, board := checkProjectBoardChangePermissions(ctx)
assert.NotNil(t, project)
assert.NotNil(t, board)
assert.False(t, ctx.Written())
}

1341
routers/web/repo/pull.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
// 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 repo
import (
"fmt"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
)
const (
tplConversation base.TplName = "repo/diff/conversation"
tplNewComment base.TplName = "repo/diff/new_comment"
)
// RenderNewCodeCommentForm will render the form for creating a new review comment
func RenderNewCodeCommentForm(ctx *context.Context) {
issue := GetActionIssue(ctx)
if !issue.IsPull {
return
}
currentReview, err := models.GetCurrentReview(ctx.User, issue)
if err != nil && !models.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
return
}
ctx.Data["PageIsPullFiles"] = true
ctx.Data["Issue"] = issue
ctx.Data["CurrentReview"] = currentReview
pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
ctx.HTML(http.StatusOK, tplNewComment)
}
// CreateCodeComment will create a code comment including an pending review if required
func CreateCodeComment(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CodeCommentForm)
issue := GetActionIssue(ctx)
if !issue.IsPull {
return
}
if ctx.Written() {
return
}
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return
}
signedLine := form.Line
if form.Side == "previous" {
signedLine *= -1
}
comment, err := pull_service.CreateCodeComment(
ctx.User,
ctx.Repo.GitRepo,
issue,
signedLine,
form.Content,
form.TreePath,
form.IsReview,
form.Reply,
form.LatestCommitID,
)
if err != nil {
ctx.ServerError("CreateCodeComment", err)
return
}
if comment == nil {
log.Trace("Comment not created: %-v #%d[%d]", ctx.Repo.Repository, issue.Index, issue.ID)
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return
}
log.Trace("Comment created: %-v #%d[%d] Comment[%d]", ctx.Repo.Repository, issue.Index, issue.ID, comment.ID)
if form.Origin == "diff" {
renderConversation(ctx, comment)
return
}
ctx.Redirect(comment.HTMLURL())
}
// UpdateResolveConversation add or remove an Conversation resolved mark
func UpdateResolveConversation(ctx *context.Context) {
origin := ctx.Query("origin")
action := ctx.Query("action")
commentID := ctx.QueryInt64("comment_id")
comment, err := models.GetCommentByID(commentID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if err = comment.LoadIssue(); err != nil {
ctx.ServerError("comment.LoadIssue", err)
return
}
var permResult bool
if permResult, err = models.CanMarkConversation(comment.Issue, ctx.User); err != nil {
ctx.ServerError("CanMarkConversation", err)
return
}
if !permResult {
ctx.Error(http.StatusForbidden)
return
}
if !comment.Issue.IsPull {
ctx.Error(http.StatusBadRequest)
return
}
if action == "Resolve" || action == "UnResolve" {
err = models.MarkConversation(comment, ctx.User, action == "Resolve")
if err != nil {
ctx.ServerError("MarkConversation", err)
return
}
} else {
ctx.Error(http.StatusBadRequest)
return
}
if origin == "diff" {
renderConversation(ctx, comment)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
func renderConversation(ctx *context.Context, comment *models.Comment) {
comments, err := models.FetchCodeCommentsByLine(comment.Issue, ctx.User, comment.TreePath, comment.Line)
if err != nil {
ctx.ServerError("FetchCodeCommentsByLine", err)
return
}
ctx.Data["PageIsPullFiles"] = true
ctx.Data["comments"] = comments
ctx.Data["CanMarkConversation"] = true
ctx.Data["Issue"] = comment.Issue
if err = comment.Issue.LoadPullRequest(); err != nil {
ctx.ServerError("comment.Issue.LoadPullRequest", err)
return
}
pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
ctx.HTML(http.StatusOK, tplConversation)
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.SubmitReviewForm)
issue := GetActionIssue(ctx)
if !issue.IsPull {
return
}
if ctx.Written() {
return
}
if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return
}
reviewType := form.ReviewType()
switch reviewType {
case models.ReviewTypeUnknown:
ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
return
// can not approve/reject your own PR
case models.ReviewTypeApprove, models.ReviewTypeReject:
if issue.IsPoster(ctx.User.ID) {
var translated string
if reviewType == models.ReviewTypeApprove {
translated = ctx.Tr("repo.issues.review.self.approval")
} else {
translated = ctx.Tr("repo.issues.review.self.rejection")
}
ctx.Flash.Error(translated)
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return
}
}
_, comm, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID)
if err != nil {
if models.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
} else {
ctx.ServerError("SubmitReview", err)
}
return
}
ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, issue.Index, comm.HashTag()))
}
// DismissReview dismissing stale review by repo admin
func DismissReview(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.DismissReviewForm)
comm, err := pull_service.DismissReview(form.ReviewID, form.Message, ctx.User, true)
if err != nil {
ctx.ServerError("pull_service.DismissReview", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
}

512
routers/web/repo/release.go Normal file
View File

@@ -0,0 +1,512 @@
// 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 repo
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
releaseservice "code.gitea.io/gitea/services/release"
)
const (
tplReleases base.TplName = "repo/release/list"
tplReleaseNew base.TplName = "repo/release/new"
)
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error {
// Fast return if release target is same as default branch.
if repoCtx.BranchName == release.Target {
release.NumCommitsBehind = repoCtx.CommitsCount - release.NumCommits
return nil
}
// Get count if not exists
if _, ok := countCache[release.Target]; !ok {
if repoCtx.GitRepo.IsBranchExist(release.Target) {
commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
if err != nil {
return fmt.Errorf("GetBranchCommit: %v", err)
}
countCache[release.Target], err = commit.CommitsCount()
if err != nil {
return fmt.Errorf("CommitsCount: %v", err)
}
} else {
// Use NumCommits of the newest release on that target
countCache[release.Target] = release.NumCommits
}
}
release.NumCommitsBehind = countCache[release.Target] - release.NumCommits
return nil
}
// Releases render releases list page
func Releases(ctx *context.Context) {
releasesOrTags(ctx, false)
}
// TagsList render tags list page
func TagsList(ctx *context.Context) {
releasesOrTags(ctx, true)
}
func releasesOrTags(ctx *context.Context, isTagList bool) {
ctx.Data["PageIsReleaseList"] = true
ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch
ctx.Data["IsViewBranch"] = false
ctx.Data["IsViewTag"] = true
// Disable the showCreateNewBranch form in the dropdown on this page.
ctx.Data["CanCreateBranch"] = false
ctx.Data["HideBranchesInDropdown"] = true
if isTagList {
ctx.Data["Title"] = ctx.Tr("repo.release.tags")
ctx.Data["PageIsTagList"] = true
} else {
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
ctx.Data["PageIsTagList"] = false
}
tags, err := ctx.Repo.GitRepo.GetTags()
if err != nil {
ctx.ServerError("GetTags", err)
return
}
ctx.Data["Tags"] = tags
writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
opts := models.FindReleasesOptions{
ListOptions: models.ListOptions{
Page: ctx.QueryInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
},
IncludeDrafts: writeAccess && !isTagList,
IncludeTags: isTagList,
}
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.ServerError("GetReleasesByRepoID", err)
return
}
count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err)
return
}
if err = models.GetReleaseAttachments(releases...); err != nil {
ctx.ServerError("GetReleaseAttachments", err)
return
}
// Temporary cache commits count of used branches to speed up.
countCache := make(map[string]int64)
cacheUsers := make(map[int64]*models.User)
if ctx.User != nil {
cacheUsers[ctx.User.ID] = ctx.User
}
var ok bool
for _, r := range releases {
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
r.Publisher, err = models.GetUserByID(r.PublisherID)
if err != nil {
if models.IsErrUserNotExist(err) {
r.Publisher = models.NewGhostUser()
} else {
ctx.ServerError("GetUserByID", err)
return
}
}
cacheUsers[r.PublisherID] = r.Publisher
}
r.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, r.Note)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
if r.IsDraft {
continue
}
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
ctx.ServerError("calReleaseNumCommitsBehind", err)
return
}
}
ctx.Data["Releases"] = releases
ctx.Data["ReleasesNum"] = len(releases)
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplReleases)
}
// SingleRelease renders a single release's page
func SingleRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
ctx.Data["PageIsReleaseList"] = true
writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("*"))
if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
return
}
ctx.ServerError("GetReleasesByRepoID", err)
return
}
err = models.GetReleaseAttachments(release)
if err != nil {
ctx.ServerError("GetReleaseAttachments", err)
return
}
release.Publisher, err = models.GetUserByID(release.PublisherID)
if err != nil {
if models.IsErrUserNotExist(err) {
release.Publisher = models.NewGhostUser()
} else {
ctx.ServerError("GetUserByID", err)
return
}
}
if !release.IsDraft {
if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
ctx.ServerError("calReleaseNumCommitsBehind", err)
return
}
}
release.Note, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
}, release.Note)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["Releases"] = []*models.Release{release}
ctx.HTML(http.StatusOK, tplReleases)
}
// LatestRelease redirects to the latest release
func LatestRelease(ctx *context.Context) {
release, err := models.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound("LatestRelease", err)
return
}
ctx.ServerError("GetLatestReleaseByRepoID", err)
return
}
if err := release.LoadAttributes(); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Redirect(release.HTMLURL())
}
// NewRelease render creating or edit release page
func NewRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
if tagName := ctx.Query("tag"); len(tagName) > 0 {
rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil && !models.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err)
return
}
if rel != nil {
rel.Repo = ctx.Repo.Repository
if err := rel.LoadAttributes(); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Data["tag_name"] = rel.TagName
ctx.Data["tag_target"] = rel.Target
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments
}
}
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "release")
ctx.HTML(http.StatusOK, tplReleaseNew)
}
// NewReleasePost response for creating a release
func NewReleasePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewReleaseForm)
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["RequireTribute"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplReleaseNew)
return
}
if !ctx.Repo.GitRepo.IsBranchExist(form.Target) {
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
return
}
var attachmentUUIDs []string
if setting.Attachment.Enabled {
attachmentUUIDs = form.Files
}
rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
if err != nil {
if !models.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err)
return
}
msg := ""
if len(form.Title) > 0 && form.AddTagMsg {
msg = form.Title + "\n\n" + form.Content
}
if len(form.TagOnly) > 0 {
if err = releaseservice.CreateNewTag(ctx.User, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil {
if models.IsErrTagAlreadyExists(err) {
e := err.(models.ErrTagAlreadyExists)
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
return
}
ctx.ServerError("releaseservice.CreateNewTag", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName))
ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + form.TagName)
return
}
rel = &models.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.User.ID,
Title: form.Title,
TagName: form.TagName,
Target: form.Target,
Note: form.Content,
IsDraft: len(form.Draft) > 0,
IsPrerelease: form.Prerelease,
IsTag: false,
}
if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil {
ctx.Data["Err_TagName"] = true
switch {
case models.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case models.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
default:
ctx.ServerError("CreateRelease", err)
}
return
}
} else {
if !rel.IsTag {
ctx.Data["Err_TagName"] = true
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
return
}
rel.Title = form.Title
rel.Note = form.Content
rel.Target = form.Target
rel.IsDraft = len(form.Draft) > 0
rel.IsPrerelease = form.Prerelease
rel.PublisherID = ctx.User.ID
rel.IsTag = false
if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil {
ctx.Data["Err_TagName"] = true
ctx.ServerError("UpdateRelease", err)
return
}
}
log.Trace("Release created: %s/%s:%s", ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
}
// EditRelease render release edit page
func EditRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "release")
tagName := ctx.Params("*")
rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
} else {
ctx.ServerError("GetRelease", err)
}
return
}
ctx.Data["ID"] = rel.ID
ctx.Data["tag_name"] = rel.TagName
ctx.Data["tag_target"] = rel.Target
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
ctx.Data["IsDraft"] = rel.IsDraft
rel.Repo = ctx.Repo.Repository
if err := rel.LoadAttributes(); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Data["attachments"] = rel.Attachments
ctx.HTML(http.StatusOK, tplReleaseNew)
}
// EditReleasePost response for edit release
func EditReleasePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditReleaseForm)
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
ctx.Data["RequireSimpleMDE"] = true
ctx.Data["RequireTribute"] = true
tagName := ctx.Params("*")
rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil {
if models.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
} else {
ctx.ServerError("GetRelease", err)
}
return
}
if rel.IsTag {
ctx.NotFound("GetRelease", err)
return
}
ctx.Data["tag_name"] = rel.TagName
ctx.Data["tag_target"] = rel.Target
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplReleaseNew)
return
}
const delPrefix = "attachment-del-"
const editPrefix = "attachment-edit-"
var addAttachmentUUIDs, delAttachmentUUIDs []string
var editAttachments = make(map[string]string) // uuid -> new name
if setting.Attachment.Enabled {
addAttachmentUUIDs = form.Files
for k, v := range ctx.Req.Form {
if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
delAttachmentUUIDs = append(delAttachmentUUIDs, k[len(delPrefix):])
} else if strings.HasPrefix(k, editPrefix) {
editAttachments[k[len(editPrefix):]] = v[0]
}
}
}
rel.Title = form.Title
rel.Note = form.Content
rel.IsDraft = len(form.Draft) > 0
rel.IsPrerelease = form.Prerelease
if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo,
rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {
ctx.ServerError("UpdateRelease", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
}
// DeleteRelease delete a release
func DeleteRelease(ctx *context.Context) {
deleteReleaseOrTag(ctx, false)
}
// DeleteTag delete a tag
func DeleteTag(ctx *context.Context) {
deleteReleaseOrTag(ctx, true)
}
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
if err := releaseservice.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, isDelTag); err != nil {
ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
} else {
if isDelTag {
ctx.Flash.Success(ctx.Tr("repo.release.deletion_tag_success"))
} else {
ctx.Flash.Success(ctx.Tr("repo.release.deletion_success"))
}
}
if isDelTag {
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/tags",
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/releases",
})
}

View File

@@ -0,0 +1,64 @@
// Copyright 2017 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 repo
import (
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
func TestNewReleasePost(t *testing.T) {
for _, testCase := range []struct {
RepoID int64
UserID int64
TagName string
Form forms.NewReleaseForm
}{
{
RepoID: 1,
UserID: 2,
TagName: "v1.1", // pre-existing tag
Form: forms.NewReleaseForm{
TagName: "newtag",
Target: "master",
Title: "title",
Content: "content",
},
},
{
RepoID: 1,
UserID: 2,
TagName: "newtag",
Form: forms.NewReleaseForm{
TagName: "newtag",
Target: "master",
Title: "title",
Content: "content",
},
},
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/releases/new")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
test.LoadGitRepo(t, ctx)
web.SetForm(ctx, &testCase.Form)
NewReleasePost(ctx)
models.AssertExistsAndLoadBean(t, &models.Release{
RepoID: 1,
PublisherID: 2,
TagName: testCase.Form.TagName,
Target: testCase.Form.Target,
Title: testCase.Form.Title,
Note: testCase.Form.Content,
}, models.Cond("is_draft=?", len(testCase.Form.Draft) > 0))
ctx.Repo.GitRepo.Close()
}
}

388
routers/web/repo/repo.go Normal file
View File

@@ -0,0 +1,388 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 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 repo
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
archiver_service "code.gitea.io/gitea/services/archiver"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
)
const (
tplCreate base.TplName = "repo/create"
tplAlertDetails base.TplName = "base/alert_details"
)
// MustBeNotEmpty render when a repo is a empty git dir
func MustBeNotEmpty(ctx *context.Context) {
if ctx.Repo.Repository.IsEmpty {
ctx.NotFound("MustBeNotEmpty", nil)
}
}
// MustBeEditable check that repo can be edited
func MustBeEditable(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
ctx.NotFound("", nil)
return
}
}
// MustBeAbleToUpload check that repo can be uploaded to
func MustBeAbleToUpload(ctx *context.Context) {
if !setting.Repository.Upload.Enabled {
ctx.NotFound("", nil)
}
}
func checkContextUser(ctx *context.Context, uid int64) *models.User {
orgs, err := models.GetOrgsCanCreateRepoByUserID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
return nil
}
if !ctx.User.IsAdmin {
orgsAvailable := []*models.User{}
for i := 0; i < len(orgs); i++ {
if orgs[i].CanCreateRepo() {
orgsAvailable = append(orgsAvailable, orgs[i])
}
}
ctx.Data["Orgs"] = orgsAvailable
} else {
ctx.Data["Orgs"] = orgs
}
// Not equal means current user is an organization.
if uid == ctx.User.ID || uid == 0 {
return ctx.User
}
org, err := models.GetUserByID(uid)
if models.IsErrUserNotExist(err) {
return ctx.User
}
if err != nil {
ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %v", uid, err))
return nil
}
// Check ownership of organization.
if !org.IsOrganization() {
ctx.Error(http.StatusForbidden)
return nil
}
if !ctx.User.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
if err != nil {
ctx.ServerError("CanCreateOrgRepo", err)
return nil
} else if !canCreate {
ctx.Error(http.StatusForbidden)
return nil
}
} else {
ctx.Data["Orgs"] = orgs
}
return org
}
func getRepoPrivate(ctx *context.Context) bool {
switch strings.ToLower(setting.Repository.DefaultPrivate) {
case setting.RepoCreatingLastUserVisibility:
return ctx.User.LastRepoVisibility
case setting.RepoCreatingPrivate:
return true
case setting.RepoCreatingPublic:
return false
default:
return ctx.User.LastRepoVisibility
}
}
// Create render creating repository page
func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_repo")
// Give default value for template to render.
ctx.Data["Gitignores"] = models.Gitignores
ctx.Data["LabelTemplates"] = models.LabelTemplates
ctx.Data["Licenses"] = models.Licenses
ctx.Data["Readmes"] = models.Readmes
ctx.Data["readme"] = "Default"
ctx.Data["private"] = getRepoPrivate(ctx)
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["default_branch"] = setting.Repository.DefaultBranch
ctxUser := checkContextUser(ctx, ctx.QueryInt64("org"))
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
templateID := ctx.QueryInt64("template_id")
if templateID > 0 {
templateRepo, err := models.GetRepositoryByID(templateID)
if err == nil && templateRepo.CheckUnitUser(ctxUser, models.UnitTypeCode) {
ctx.Data["repo_template"] = templateID
ctx.Data["repo_template_name"] = templateRepo.Name
}
}
ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
ctx.HTML(http.StatusOK, tplCreate)
}
func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) {
switch {
case models.IsErrReachLimitOfRepo(err):
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
case models.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
case models.IsErrRepoFilesAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
switch {
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
default:
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
}
case models.IsErrNameReserved(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
default:
ctx.ServerError(name, err)
}
}
// CreatePost response for creating repository
func CreatePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateRepoForm)
ctx.Data["Title"] = ctx.Tr("new_repo")
ctx.Data["Gitignores"] = models.Gitignores
ctx.Data["LabelTemplates"] = models.LabelTemplates
ctx.Data["Licenses"] = models.Licenses
ctx.Data["Readmes"] = models.Readmes
ctx.Data["CanCreateRepo"] = ctx.User.CanCreateRepo()
ctx.Data["MaxCreationLimit"] = ctx.User.MaxCreationLimit()
ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplCreate)
return
}
var repo *models.Repository
var err error
if form.RepoTemplate > 0 {
opts := models.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private,
GitContent: form.GitContent,
Topics: form.Topics,
GitHooks: form.GitHooks,
Webhooks: form.Webhooks,
Avatar: form.Avatar,
IssueLabels: form.Labels,
}
if !opts.IsValid() {
ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
return
}
templateRepo := getRepository(ctx, form.RepoTemplate)
if ctx.Written() {
return
}
if !templateRepo.IsTemplate {
ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
return
}
repo, err = repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts)
if err == nil {
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
return
}
} else {
repo, err = repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Gitignores: form.Gitignores,
IssueLabels: form.IssueLabels,
License: form.License,
Readme: form.Readme,
IsPrivate: form.Private || setting.Repository.ForcePrivate,
DefaultBranch: form.DefaultBranch,
AutoInit: form.AutoInit,
IsTemplate: form.Template,
TrustModel: models.ToTrustModel(form.TrustModel),
})
if err == nil {
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
return
}
}
handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
}
// Action response for actions to a repository
func Action(ctx *context.Context) {
var err error
switch ctx.Params(":action") {
case "watch":
err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
case "unwatch":
err = models.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
case "star":
err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
case "unstar":
err = models.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true)
case "reject_transfer":
err = acceptOrRejectRepoTransfer(ctx, false)
case "desc": // FIXME: this is not used
if !ctx.Repo.IsOwner() {
ctx.Error(http.StatusNotFound)
return
}
ctx.Repo.Repository.Description = ctx.Query("desc")
ctx.Repo.Repository.Website = ctx.Query("site")
err = models.UpdateRepository(ctx.Repo.Repository, false)
}
if err != nil {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
return
}
ctx.RedirectToFirst(ctx.Query("redirect_to"), ctx.Repo.RepoLink)
}
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
if err != nil {
return err
}
if err := repoTransfer.LoadAttributes(); err != nil {
return err
}
if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
return errors.New("user does not have enough permissions")
}
if accept {
if err := repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
} else {
if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
}
ctx.Redirect(ctx.Repo.Repository.HTMLURL())
return nil
}
// RedirectDownload return a file based on the following infos:
func RedirectDownload(ctx *context.Context) {
var (
vTag = ctx.Params("vTag")
fileName = ctx.Params("fileName")
)
tagNames := []string{vTag}
curRepo := ctx.Repo.Repository
releases, err := models.GetReleasesByRepoIDAndNames(models.DefaultDBContext(), curRepo.ID, tagNames)
if err != nil {
if models.IsErrAttachmentNotExist(err) {
ctx.Error(http.StatusNotFound)
return
}
ctx.ServerError("RedirectDownload", err)
return
}
if len(releases) == 1 {
release := releases[0]
att, err := models.GetAttachmentByReleaseIDFileName(release.ID, fileName)
if err != nil {
ctx.Error(http.StatusNotFound)
return
}
if att != nil {
ctx.Redirect(att.DownloadURL())
return
}
}
ctx.Error(http.StatusNotFound)
}
// InitiateDownload will enqueue an archival request, as needed. It may submit
// a request that's already in-progress, but the archiver service will just
// kind of drop it on the floor if this is the case.
func InitiateDownload(ctx *context.Context) {
uri := ctx.Params("*")
aReq := archiver_service.DeriveRequestFrom(ctx, uri)
if aReq == nil {
ctx.Error(http.StatusNotFound)
return
}
complete := aReq.IsComplete()
if !complete {
aReq = archiver_service.ArchiveRepository(aReq)
complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second)
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"complete": complete,
})
}

View File

@@ -0,0 +1,55 @@
// Copyright 2017 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 repo
import (
"net/http"
"strings"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
)
const tplSearch base.TplName = "repo/search"
// Search render repository search page
func Search(ctx *context.Context) {
if !setting.Indexer.RepoIndexerEnabled {
ctx.Redirect(ctx.Repo.RepoLink, 302)
return
}
language := strings.TrimSpace(ctx.Query("l"))
keyword := strings.TrimSpace(ctx.Query("q"))
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
queryType := strings.TrimSpace(ctx.Query("t"))
isMatch := queryType == "match"
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch([]int64{ctx.Repo.Repository.ID},
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
if err != nil {
ctx.ServerError("SearchResults", err)
return
}
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
ctx.Data["queryType"] = queryType
ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages
ctx.Data["RequireHighlightJS"] = true
ctx.Data["PageIsViewCode"] = true
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSearch)
}

1053
routers/web/repo/setting.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,286 @@
// Copyright 2017 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 repo
import (
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
)
// ProtectedBranch render the page to protect the repository
func ProtectedBranch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
protectedBranches, err := ctx.Repo.Repository.GetProtectedBranches()
if err != nil {
ctx.ServerError("GetProtectedBranches", err)
return
}
ctx.Data["ProtectedBranches"] = protectedBranches
branches := ctx.Data["Branches"].([]string)
leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
for _, b := range branches {
var protected bool
for _, pb := range protectedBranches {
if b == pb.BranchName {
protected = true
break
}
}
if !protected {
leftBranches = append(leftBranches, b)
}
}
ctx.Data["LeftBranches"] = leftBranches
ctx.HTML(http.StatusOK, tplBranches)
}
// ProtectedBranchPost response for protect for a branch of a repository
func ProtectedBranchPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
repo := ctx.Repo.Repository
switch ctx.Query("action") {
case "default_branch":
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplBranches)
return
}
branch := ctx.Query("branch")
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
ctx.Status(404)
return
} else if repo.DefaultBranch != branch {
repo.DefaultBranch = branch
if err := ctx.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.ServerError("SetDefaultBranch", err)
return
}
}
if err := repo.UpdateDefaultBranch(); err != nil {
ctx.ServerError("SetDefaultBranch", err)
return
}
}
log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
default:
ctx.NotFound("", nil)
}
}
// SettingsProtectedBranch renders the protected branch setting page
func SettingsProtectedBranch(c *context.Context) {
branch := c.Params("*")
if !c.Repo.GitRepo.IsBranchExist(branch) {
c.NotFound("IsBranchExist", nil)
return
}
c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + branch
c.Data["PageIsSettingsBranches"] = true
protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
if err != nil {
if !git.IsErrBranchNotExist(err) {
c.ServerError("GetProtectBranchOfRepoByName", err)
return
}
}
if protectBranch == nil {
// No options found, create defaults.
protectBranch = &models.ProtectedBranch{
BranchName: branch,
}
}
users, err := c.Repo.Repository.GetReaders()
if err != nil {
c.ServerError("Repo.Repository.GetReaders", err)
return
}
c.Data["Users"] = users
c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
contexts, _ := models.FindRepoRecentCommitStatusContexts(c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
for _, ctx := range protectBranch.StatusCheckContexts {
var found bool
for i := range contexts {
if contexts[i] == ctx {
found = true
break
}
}
if !found {
contexts = append(contexts, ctx)
}
}
c.Data["branch_status_check_contexts"] = contexts
c.Data["is_context_required"] = func(context string) bool {
for _, c := range protectBranch.StatusCheckContexts {
if c == context {
return true
}
}
return false
}
if c.Repo.Owner.IsOrganization() {
teams, err := c.Repo.Owner.TeamsWithAccessToRepo(c.Repo.Repository.ID, models.AccessModeRead)
if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return
}
c.Data["Teams"] = teams
c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",")
}
c.Data["Branch"] = protectBranch
c.HTML(http.StatusOK, tplProtectedBranch)
}
// SettingsProtectedBranchPost updates the protected branch settings
func SettingsProtectedBranchPost(ctx *context.Context) {
f := web.GetForm(ctx).(*forms.ProtectBranchForm)
branch := ctx.Params("*")
if !ctx.Repo.GitRepo.IsBranchExist(branch) {
ctx.NotFound("IsBranchExist", nil)
return
}
protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
if err != nil {
if !git.IsErrBranchNotExist(err) {
ctx.ServerError("GetProtectBranchOfRepoByName", err)
return
}
}
if f.Protected {
if protectBranch == nil {
// No options found, create defaults.
protectBranch = &models.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID,
BranchName: branch,
}
}
if f.RequiredApprovals < 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
}
var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
switch f.EnablePush {
case "all":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
case "whitelist":
protectBranch.CanPush = true
protectBranch.EnableWhitelist = true
protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
if strings.TrimSpace(f.WhitelistUsers) != "" {
whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
}
if strings.TrimSpace(f.WhitelistTeams) != "" {
whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
}
default:
protectBranch.CanPush = false
protectBranch.EnableWhitelist = false
protectBranch.WhitelistDeployKeys = false
}
protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
if f.EnableMergeWhitelist {
if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
}
if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
}
}
protectBranch.EnableStatusCheck = f.EnableStatusCheck
if f.EnableStatusCheck {
protectBranch.StatusCheckContexts = f.StatusCheckContexts
} else {
protectBranch.StatusCheckContexts = nil
}
protectBranch.RequiredApprovals = f.RequiredApprovals
protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
if f.EnableApprovalsWhitelist {
if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
}
if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
}
}
protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
protectBranch.RequireSignedCommits = f.RequireSignedCommits
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
})
if err != nil {
ctx.ServerError("UpdateProtectBranch", err)
return
}
if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
ctx.ServerError("CheckPrsForBaseBranch", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, branch))
} else {
if protectBranch != nil {
if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil {
ctx.ServerError("DeleteProtectedBranch", err)
return
}
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
}
}

View File

@@ -0,0 +1,413 @@
// Copyright 2017 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 repo
import (
"io/ioutil"
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
)
func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
tmpDir, err := ioutil.TempDir("", "tmp-ssh")
if err != nil {
assert.Fail(t, "Unable to create temporary directory: %v", err)
return nil
}
oldPath := setting.SSH.RootPath
setting.SSH.RootPath = tmpDir
return func() {
setting.SSH.RootPath = oldPath
util.RemoveAll(tmpDir)
}
}
func TestAddReadOnlyDeployKey(t *testing.T) {
if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
defer deferable()
} else {
return
}
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/settings/keys")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 2)
addKeyForm := forms.AddKeyForm{
Title: "read-only",
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.DeployKey{
Name: addKeyForm.Title,
Content: addKeyForm.Content,
Mode: models.AccessModeRead,
})
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
defer deferable()
} else {
return
}
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/settings/keys")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 2)
addKeyForm := forms.AddKeyForm{
Title: "read-write",
Content: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
IsWritable: true,
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.DeployKey{
Name: addKeyForm.Title,
Content: addKeyForm.Content,
Mode: models.AccessModeWrite,
})
}
func TestCollaborationPost(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadUser(t, ctx, 4)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("collaborator", "user4")
u := &models.User{
LowerName: "user2",
Type: models.UserTypeIndividual,
}
re := &models.Repository{
ID: 2,
Owner: u,
}
repo := &context.Repository{
Owner: u,
Repository: re,
}
ctx.Repo = repo
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
exists, err := re.IsCollaborator(4)
assert.NoError(t, err)
assert.True(t, exists)
}
func TestCollaborationPost_InactiveUser(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadUser(t, ctx, 9)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("collaborator", "user9")
repo := &context.Repository{
Owner: &models.User{
LowerName: "user2",
},
}
ctx.Repo = repo
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadUser(t, ctx, 4)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("collaborator", "user4")
u := &models.User{
LowerName: "user2",
Type: models.UserTypeIndividual,
}
re := &models.Repository{
ID: 2,
Owner: u,
}
repo := &context.Repository{
Owner: u,
Repository: re,
}
ctx.Repo = repo
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
exists, err := re.IsCollaborator(4)
assert.NoError(t, err)
assert.True(t, exists)
// Try adding the same collaborator again
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestCollaborationPost_NonExistentUser(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/issues/labels")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("collaborator", "user34")
repo := &context.Repository{
Owner: &models.User{
LowerName: "user2",
},
}
ctx.Repo = repo
CollaborationPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestAddTeamPost(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "org26/repo43")
ctx.Req.Form.Set("team", "team11")
org := &models.User{
LowerName: "org26",
Type: models.UserTypeOrganization,
}
team := &models.Team{
ID: 11,
OrgID: 26,
}
re := &models.Repository{
ID: 43,
Owner: org,
OwnerID: 26,
}
repo := &context.Repository{
Owner: &models.User{
ID: 26,
LowerName: "org26",
RepoAdminChangeTeamAccess: true,
},
Repository: re,
}
ctx.Repo = repo
AddTeamPost(ctx)
assert.True(t, team.HasRepository(re.ID))
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
func TestAddTeamPost_NotAllowed(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "org26/repo43")
ctx.Req.Form.Set("team", "team11")
org := &models.User{
LowerName: "org26",
Type: models.UserTypeOrganization,
}
team := &models.Team{
ID: 11,
OrgID: 26,
}
re := &models.Repository{
ID: 43,
Owner: org,
OwnerID: 26,
}
repo := &context.Repository{
Owner: &models.User{
ID: 26,
LowerName: "org26",
RepoAdminChangeTeamAccess: false,
},
Repository: re,
}
ctx.Repo = repo
AddTeamPost(ctx)
assert.False(t, team.HasRepository(re.ID))
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestAddTeamPost_AddTeamTwice(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "org26/repo43")
ctx.Req.Form.Set("team", "team11")
org := &models.User{
LowerName: "org26",
Type: models.UserTypeOrganization,
}
team := &models.Team{
ID: 11,
OrgID: 26,
}
re := &models.Repository{
ID: 43,
Owner: org,
OwnerID: 26,
}
repo := &context.Repository{
Owner: &models.User{
ID: 26,
LowerName: "org26",
RepoAdminChangeTeamAccess: true,
},
Repository: re,
}
ctx.Repo = repo
AddTeamPost(ctx)
AddTeamPost(ctx)
assert.True(t, team.HasRepository(re.ID))
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestAddTeamPost_NonExistentTeam(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "org26/repo43")
ctx.Req.Form.Set("team", "team-non-existent")
org := &models.User{
LowerName: "org26",
Type: models.UserTypeOrganization,
}
re := &models.Repository{
ID: 43,
Owner: org,
OwnerID: 26,
}
repo := &context.Repository{
Owner: &models.User{
ID: 26,
LowerName: "org26",
RepoAdminChangeTeamAccess: true,
},
Repository: re,
}
ctx.Repo = repo
AddTeamPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
func TestDeleteTeam(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "org3/team1/repo3")
ctx.Req.Form.Set("id", "2")
org := &models.User{
LowerName: "org3",
Type: models.UserTypeOrganization,
}
team := &models.Team{
ID: 2,
OrgID: 3,
}
re := &models.Repository{
ID: 3,
Owner: org,
OwnerID: 3,
}
repo := &context.Repository{
Owner: &models.User{
ID: 3,
LowerName: "org3",
RepoAdminChangeTeamAccess: true,
},
Repository: re,
}
ctx.Repo = repo
DeleteTeam(ctx)
assert.False(t, team.HasRepository(re.ID))
}

61
routers/web/repo/topic.go Normal file
View File

@@ -0,0 +1,61 @@
// 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 repo
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
// TopicsPost response for creating repository
func TopicsPost(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"message": "Only owners could change the topics.",
})
return
}
var topics = make([]string, 0)
var topicsStr = strings.TrimSpace(ctx.Query("topics"))
if len(topicsStr) > 0 {
topics = strings.Split(topicsStr, ",")
}
validTopics, invalidTopics := models.SanitizeAndValidateTopics(topics)
if len(validTopics) > 25 {
ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
"invalidTopics": nil,
"message": ctx.Tr("repo.topic.count_prompt"),
})
return
}
if len(invalidTopics) > 0 {
ctx.JSON(http.StatusUnprocessableEntity, map[string]interface{}{
"invalidTopics": invalidTopics,
"message": ctx.Tr("repo.topic.format_prompt"),
})
return
}
err := models.SaveTopics(ctx.Repo.Repository.ID, validTopics...)
if err != nil {
log.Error("SaveTopics failed: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"message": "Save topics failed.",
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": "ok",
})
}

808
routers/web/repo/view.go Normal file
View File

@@ -0,0 +1,808 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// 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 repo
import (
"bytes"
"encoding/base64"
"fmt"
gotemplate "html/template"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
)
const (
tplRepoEMPTY base.TplName = "repo/empty"
tplRepoHome base.TplName = "repo/home"
tplWatchers base.TplName = "repo/watchers"
tplForks base.TplName = "repo/forks"
tplMigrating base.TplName = "repo/migrate/migrating"
)
type namedBlob struct {
name string
isSymlink bool
blob *git.Blob
}
func linesBytesCount(s []byte) int {
nl := []byte{'\n'}
n := bytes.Count(s, nl)
if len(s) > 0 && !bytes.HasSuffix(s, nl) {
n++
}
return n
}
// FIXME: There has to be a more efficient way of doing this
func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) {
tree, err := commit.SubTree(treePath)
if err != nil {
return nil, err
}
entries, err := tree.ListEntries()
if err != nil {
return nil, err
}
var readmeFiles [4]*namedBlob
var exts = []string{".md", ".txt", ""} // sorted by priority
for _, entry := range entries {
if entry.IsDir() {
continue
}
for i, ext := range exts {
if markup.IsReadmeFile(entry.Name(), ext) {
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
name := entry.Name()
isSymlink := entry.IsLink()
target := entry
if isSymlink {
target, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
return nil, err
}
}
if target != nil && (target.IsExecutable() || target.IsRegular()) {
readmeFiles[i] = &namedBlob{
name,
isSymlink,
target.Blob(),
}
}
}
}
}
if markup.IsReadmeFile(entry.Name()) {
if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) {
name := entry.Name()
isSymlink := entry.IsLink()
if isSymlink {
entry, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
return nil, err
}
}
if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
readmeFiles[3] = &namedBlob{
name,
isSymlink,
entry.Blob(),
}
}
}
}
}
var readmeFile *namedBlob
for _, f := range readmeFiles {
if f != nil {
readmeFile = f
break
}
}
return readmeFile, nil
}
func renderDirectory(ctx *context.Context, treeLink string) {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
return
}
entries, err := tree.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
entries.CustomSort(base.NaturalSortLess)
var c *git.LastCommitCache
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
}
var latestCommit *git.Commit
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return
}
// 3 for the extensions in exts[] in order
// the last one is for a readme that doesn't
// strictly match an extension
var readmeFiles [4]*namedBlob
var docsEntries [3]*git.TreeEntry
var exts = []string{".md", ".txt", ""} // sorted by priority
for _, entry := range entries {
if entry.IsDir() {
lowerName := strings.ToLower(entry.Name())
switch lowerName {
case "docs":
if entry.Name() == "docs" || docsEntries[0] == nil {
docsEntries[0] = entry
}
case ".gitea":
if entry.Name() == ".gitea" || docsEntries[1] == nil {
docsEntries[1] = entry
}
case ".github":
if entry.Name() == ".github" || docsEntries[2] == nil {
docsEntries[2] = entry
}
}
continue
}
for i, ext := range exts {
if markup.IsReadmeFile(entry.Name(), ext) {
log.Debug("%s", entry.Name())
name := entry.Name()
isSymlink := entry.IsLink()
target := entry
if isSymlink {
target, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
ctx.ServerError("FollowLinks", err)
return
}
}
log.Debug("%t", target == nil)
if target != nil && (target.IsExecutable() || target.IsRegular()) {
readmeFiles[i] = &namedBlob{
name,
isSymlink,
target.Blob(),
}
}
}
}
if markup.IsReadmeFile(entry.Name()) {
name := entry.Name()
isSymlink := entry.IsLink()
if isSymlink {
entry, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
ctx.ServerError("FollowLinks", err)
return
}
}
if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
readmeFiles[3] = &namedBlob{
name,
isSymlink,
entry.Blob(),
}
}
}
}
var readmeFile *namedBlob
readmeTreelink := treeLink
for _, f := range readmeFiles {
if f != nil {
readmeFile = f
break
}
}
if ctx.Repo.TreePath == "" && readmeFile == nil {
for _, entry := range docsEntries {
if entry == nil {
continue
}
readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
if err != nil {
ctx.ServerError("getReadmeFileFromPath", err)
return
}
if readmeFile != nil {
readmeFile.name = entry.Name() + "/" + readmeFile.name
readmeTreelink = treeLink + "/" + entry.GetSubJumpablePathName()
break
}
}
}
if readmeFile != nil {
ctx.Data["RawFileLink"] = ""
ctx.Data["ReadmeInList"] = true
ctx.Data["ReadmeExist"] = true
ctx.Data["FileIsSymlink"] = readmeFile.isSymlink
dataRc, err := readmeFile.blob.DataAsync()
if err != nil {
ctx.ServerError("Data", err)
return
}
defer dataRc.Close()
buf := make([]byte, 1024)
n, _ := dataRc.Read(buf)
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
isTextFile := st.IsText()
ctx.Data["FileIsText"] = isTextFile
ctx.Data["FileName"] = readmeFile.name
fileSize := int64(0)
isLFSFile := false
ctx.Data["IsLFSFile"] = false
// FIXME: what happens when README file is an image?
if isTextFile && setting.LFS.StartServer {
pointer, _ := lfs.ReadPointerFromBuffer(buf)
if pointer.IsValid() {
meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
if err != nil && err != models.ErrLFSObjectNotExist {
ctx.ServerError("GetLFSMetaObject", err)
return
}
if meta != nil {
ctx.Data["IsLFSFile"] = true
isLFSFile = true
// OK read the lfs object
var err error
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
ctx.ServerError("ReadMetaObject", err)
return
}
defer dataRc.Close()
buf = make([]byte, 1024)
n, err = dataRc.Read(buf)
if err != nil {
ctx.ServerError("Data", err)
return
}
buf = buf[:n]
st = typesniffer.DetectContentType(buf)
isTextFile = st.IsText()
ctx.Data["IsTextFile"] = isTextFile
fileSize = meta.Size
ctx.Data["FileSize"] = meta.Size
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64)
}
}
}
if !isLFSFile {
fileSize = readmeFile.blob.Size()
}
if isTextFile {
if fileSize >= setting.UI.MaxDisplayFileSize {
// Pretend that this is a normal text file to display 'This file is too large to be shown'
ctx.Data["IsFileTooLarge"] = true
ctx.Data["IsTextFile"] = true
ctx.Data["FileSize"] = fileSize
} else {
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
if markupType := markup.Type(readmeFile.name); markupType != "" {
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = string(markupType)
var result strings.Builder
err := markup.Render(&markup.RenderContext{
Filename: readmeFile.name,
URLPrefix: readmeTreelink,
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
}, rd, &result)
if err != nil {
log.Error("Render failed: %v then fallback", err)
bs, _ := ioutil.ReadAll(rd)
ctx.Data["FileContent"] = strings.ReplaceAll(
gotemplate.HTMLEscapeString(string(bs)), "\n", `<br>`,
)
} else {
ctx.Data["FileContent"] = result.String()
}
} else {
ctx.Data["IsRenderedHTML"] = true
ctx.Data["FileContent"] = strings.ReplaceAll(
gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
)
}
}
}
}
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
verification := models.ParseCommitWithSignature(latestCommit)
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
// Check permission to add or upload new file.
if ctx.Repo.CanWrite(models.UnitTypeCode) && ctx.Repo.IsViewBranch {
ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
}
ctx.Data["SSHDomain"] = setting.SSH.Domain
}
func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) {
ctx.Data["IsViewFile"] = true
blob := entry.Blob()
dataRc, err := blob.DataAsync()
if err != nil {
ctx.ServerError("DataAsync", err)
return
}
defer dataRc.Close()
ctx.Data["Title"] = ctx.Data["Title"].(string) + " - " + ctx.Repo.TreePath + " at " + ctx.Repo.BranchName
fileSize := blob.Size()
ctx.Data["FileIsSymlink"] = entry.IsLink()
ctx.Data["FileName"] = blob.Name()
ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath
buf := make([]byte, 1024)
n, _ := dataRc.Read(buf)
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
isTextFile := st.IsText()
isLFSFile := false
isDisplayingSource := ctx.Query("display") == "source"
isDisplayingRendered := !isDisplayingSource
//Check for LFS meta file
if isTextFile && setting.LFS.StartServer {
pointer, _ := lfs.ReadPointerFromBuffer(buf)
if pointer.IsValid() {
meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(pointer.Oid)
if err != nil && err != models.ErrLFSObjectNotExist {
ctx.ServerError("GetLFSMetaObject", err)
return
}
if meta != nil {
isLFSFile = true
// OK read the lfs object
var err error
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
ctx.ServerError("ReadMetaObject", err)
return
}
defer dataRc.Close()
buf = make([]byte, 1024)
n, err = dataRc.Read(buf)
// Error EOF don't mean there is an error, it just means we read to
// the end
if err != nil && err != io.EOF {
ctx.ServerError("Data", err)
return
}
buf = buf[:n]
st = typesniffer.DetectContentType(buf)
isTextFile = st.IsText()
fileSize = meta.Size
ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath)
}
}
}
isRepresentableAsText := st.IsRepresentableAsText()
if !isRepresentableAsText {
// If we can't show plain text, always try to render.
isDisplayingSource = false
isDisplayingRendered = true
}
ctx.Data["IsLFSFile"] = isLFSFile
ctx.Data["FileSize"] = fileSize
ctx.Data["IsTextFile"] = isTextFile
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
ctx.Data["IsDisplayingSource"] = isDisplayingSource
ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource
// Check LFS Lock
lfsLock, err := ctx.Repo.Repository.GetTreePathLock(ctx.Repo.TreePath)
ctx.Data["LFSLock"] = lfsLock
if err != nil {
ctx.ServerError("GetTreePathLock", err)
return
}
if lfsLock != nil {
ctx.Data["LFSLockOwner"] = lfsLock.Owner.DisplayName()
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}
// Assume file is not editable first.
if isLFSFile {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
} else if !isRepresentableAsText {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
}
switch {
case isRepresentableAsText:
if st.IsSvgImage() {
ctx.Data["IsImageFile"] = true
ctx.Data["HasSourceRenderedToggle"] = true
}
if fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
readmeExist := markup.IsReadmeFile(blob.Name())
ctx.Data["ReadmeExist"] = readmeExist
if markupType := markup.Type(blob.Name()); markupType != "" {
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
var result strings.Builder
err := markup.Render(&markup.RenderContext{
Filename: blob.Name(),
URLPrefix: path.Dir(treeLink),
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
}, rd, &result)
if err != nil {
ctx.ServerError("Render", err)
return
}
ctx.Data["FileContent"] = result.String()
} else if readmeExist {
buf, _ := ioutil.ReadAll(rd)
ctx.Data["IsRenderedHTML"] = true
ctx.Data["FileContent"] = strings.ReplaceAll(
gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
)
} else {
buf, _ := ioutil.ReadAll(rd)
lineNums := linesBytesCount(buf)
ctx.Data["NumLines"] = strconv.Itoa(lineNums)
ctx.Data["NumLinesSet"] = true
ctx.Data["FileContent"] = highlight.File(lineNums, blob.Name(), buf)
}
if !isLFSFile {
if ctx.Repo.CanEnableEditor() {
if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
ctx.Data["CanEditFile"] = false
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
} else {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
}
} else if !ctx.Repo.IsViewBranch {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
} else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
}
}
case st.IsPDF():
ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
ctx.Data["IsAudioFile"] = true
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
default:
if fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
if markupType := markup.Type(blob.Name()); markupType != "" {
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
var result strings.Builder
err := markup.Render(&markup.RenderContext{
Filename: blob.Name(),
URLPrefix: path.Dir(treeLink),
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
}, rd, &result)
if err != nil {
ctx.ServerError("Render", err)
return
}
ctx.Data["FileContent"] = result.String()
}
}
if ctx.Repo.CanEnableEditor() {
if lfsLock != nil && lfsLock.OwnerID != ctx.User.ID {
ctx.Data["CanDeleteFile"] = false
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
} else {
ctx.Data["CanDeleteFile"] = true
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
}
} else if !ctx.Repo.IsViewBranch {
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
} else if !ctx.Repo.CanWrite(models.UnitTypeCode) {
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
}
}
func safeURL(address string) string {
u, err := url.Parse(address)
if err != nil {
return address
}
u.User = nil
return u.String()
}
// Home render repository home page
func Home(ctx *context.Context) {
if len(ctx.Repo.Units) > 0 {
if ctx.Repo.Repository.IsBeingCreated() {
task, err := models.GetMigratingTask(ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("models.GetMigratingTask", err)
return
}
cfg, err := task.MigrateConfig()
if err != nil {
ctx.ServerError("task.MigrateConfig", err)
return
}
ctx.Data["Repo"] = ctx.Repo
ctx.Data["MigrateTask"] = task
ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr)
ctx.HTML(http.StatusOK, tplMigrating)
return
}
if ctx.IsSigned {
// Set repo notification-status read if unread
if err := ctx.Repo.Repository.ReadBy(ctx.User.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
}
var firstUnit *models.Unit
for _, repoUnit := range ctx.Repo.Units {
if repoUnit.Type == models.UnitTypeCode {
renderCode(ctx)
return
}
unit, ok := models.Units[repoUnit.Type]
if ok && (firstUnit == nil || !firstUnit.IsLessThan(unit)) {
firstUnit = &unit
}
}
if firstUnit != nil {
ctx.Redirect(fmt.Sprintf("%s/%s%s", setting.AppSubURL, ctx.Repo.Repository.FullName(), firstUnit.URI))
return
}
}
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
}
func renderLanguageStats(ctx *context.Context) {
langs, err := ctx.Repo.Repository.GetTopLanguageStats(5)
if err != nil {
ctx.ServerError("Repo.GetTopLanguageStats", err)
return
}
ctx.Data["LanguageStats"] = langs
}
func renderRepoTopics(ctx *context.Context) {
topics, err := models.FindTopics(&models.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.ServerError("models.FindTopics", err)
return
}
ctx.Data["Topics"] = topics
}
func renderCode(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
if ctx.Repo.Repository.IsEmpty {
ctx.HTML(http.StatusOK, tplRepoEMPTY)
return
}
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
if len(ctx.Repo.Repository.Description) > 0 {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
if len(ctx.Repo.TreePath) > 0 {
treeLink += "/" + ctx.Repo.TreePath
}
// Get Topics of this repo
renderRepoTopics(ctx)
if ctx.Written() {
return
}
// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
ctx.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
renderLanguageStats(ctx)
if ctx.Written() {
return
}
if entry.IsDir() {
renderDirectory(ctx, treeLink)
} else {
renderFile(ctx, entry, treeLink, rawLink)
}
if ctx.Written() {
return
}
var treeNames []string
paths := make([]string, 0, 5)
if len(ctx.Repo.TreePath) > 0 {
treeNames = strings.Split(ctx.Repo.TreePath, "/")
for i := range treeNames {
paths = append(paths, strings.Join(treeNames[:i+1], "/"))
}
ctx.Data["HasParentPath"] = true
if len(paths)-2 >= 0 {
ctx.Data["ParentPath"] = "/" + paths[len(paths)-2]
}
}
ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
ctx.Data["BranchLink"] = branchLink
ctx.HTML(http.StatusOK, tplRepoHome)
}
// RenderUserCards render a page show users according the input templaet
func RenderUserCards(ctx *context.Context, total int, getter func(opts models.ListOptions) ([]*models.User, error), tpl base.TplName) {
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
pager := context.NewPagination(total, models.ItemsPerPage, page, 5)
ctx.Data["Page"] = pager
items, err := getter(models.ListOptions{
Page: pager.Paginater.Current(),
PageSize: models.ItemsPerPage,
})
if err != nil {
ctx.ServerError("getter", err)
return
}
ctx.Data["Cards"] = items
ctx.HTML(http.StatusOK, tpl)
}
// Watchers render repository's watch users
func Watchers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.watchers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
ctx.Data["PageIsWatchers"] = true
RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, ctx.Repo.Repository.GetWatchers, tplWatchers)
}
// Stars render repository's starred users
func Stars(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.stargazers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
ctx.Data["PageIsStargazers"] = true
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers, tplWatchers)
}
// Forks render repository's forked users
func Forks(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repos.forks")
// TODO: need pagination
forks, err := ctx.Repo.Repository.GetForks(models.ListOptions{})
if err != nil {
ctx.ServerError("GetForks", err)
return
}
for _, fork := range forks {
if err = fork.GetOwner(); err != nil {
ctx.ServerError("GetOwner", err)
return
}
}
ctx.Data["Forks"] = forks
ctx.HTML(http.StatusOK, tplForks)
}

1131
routers/web/repo/webhook.go Normal file

File diff suppressed because it is too large Load Diff

684
routers/web/repo/wiki.go Normal file
View File

@@ -0,0 +1,684 @@
// Copyright 2015 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 repo
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/forms"
wiki_service "code.gitea.io/gitea/services/wiki"
)
const (
tplWikiStart base.TplName = "repo/wiki/start"
tplWikiView base.TplName = "repo/wiki/view"
tplWikiRevision base.TplName = "repo/wiki/revision"
tplWikiNew base.TplName = "repo/wiki/new"
tplWikiPages base.TplName = "repo/wiki/pages"
)
// MustEnableWiki check if wiki is enabled, if external then redirect
func MustEnableWiki(ctx *context.Context) {
if !ctx.Repo.CanRead(models.UnitTypeWiki) &&
!ctx.Repo.CanRead(models.UnitTypeExternalWiki) {
if log.IsTrace() {
log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+
"User in repo has Permissions: %-+v",
ctx.User,
models.UnitTypeWiki,
models.UnitTypeExternalWiki,
ctx.Repo.Repository,
ctx.Repo.Permission)
}
ctx.NotFound("MustEnableWiki", nil)
return
}
unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki)
if err == nil {
ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
return
}
}
// PageMeta wiki page meta information
type PageMeta struct {
Name string
SubURL string
UpdatedUnix timeutil.TimeStamp
}
// findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target)
if err != nil && !git.IsErrNotExist(err) {
return nil, err
}
if entry != nil {
return entry, nil
}
// Then the unescaped, shortest alternative
var unescapedTarget string
if unescapedTarget, err = url.QueryUnescape(target); err != nil {
return nil, err
}
return commit.GetTreeEntryByPath(unescapedTarget)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil, nil, err
}
commit, err := wikiRepo.GetBranchCommit("master")
if err != nil {
return wikiRepo, nil, err
}
return wikiRepo, commit, nil
}
// wikiContentsByEntry returns the contents of the wiki page referenced by the
// given tree entry. Writes to ctx if an error occurs.
func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
reader, err := entry.Blob().DataAsync()
if err != nil {
ctx.ServerError("Blob.Data", err)
return nil
}
defer reader.Close()
content, err := ioutil.ReadAll(reader)
if err != nil {
ctx.ServerError("ReadAll", err)
return nil
}
return content
}
// wikiContentsByName returns the contents of a wiki page, along with a boolean
// indicating whether the page exists. Writes to ctx if an error occurs.
func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
pageFilename := wiki_service.NameToFilename(wikiName)
entry, err := findEntryForFile(commit, pageFilename)
if err != nil && !git.IsErrNotExist(err) {
ctx.ServerError("findEntryForFile", err)
return nil, nil, "", false
} else if entry == nil {
return nil, nil, "", true
}
return wikiContentsByEntry(ctx, entry), entry, pageFilename, false
}
func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
wikiRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
// Get page list.
entries, err := commit.ListEntries()
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("ListEntries", err)
return nil, nil
}
pages := make([]PageMeta, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
if err != nil {
if models.IsErrWikiInvalidFileName(err) {
continue
}
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("WikiFilenameToName", err)
return nil, nil
} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
continue
}
pages = append(pages, PageMeta{
Name: wikiName,
SubURL: wiki_service.NameToSubURL(wikiName),
})
}
ctx.Data["Pages"] = pages
// get requested pagename
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
if len(pageName) == 0 {
pageName = "Home"
}
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
ctx.Data["RequireHighlightJS"] = true
//lookup filename in wiki - get filecontent, gitTree entry , real filename
data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
}
if entry == nil || ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return nil, nil
}
sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
if ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return nil, nil
}
footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
if ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return nil, nil
}
var rctx = &markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
IsWiki: true,
}
var buf strings.Builder
if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
ctx.ServerError("Render", err)
return nil, nil
}
ctx.Data["content"] = buf.String()
buf.Reset()
if err := markdown.Render(rctx, bytes.NewReader(sidebarContent), &buf); err != nil {
ctx.ServerError("Render", err)
return nil, nil
}
ctx.Data["sidebarPresent"] = sidebarContent != nil
ctx.Data["sidebarContent"] = buf.String()
buf.Reset()
if err := markdown.Render(rctx, bytes.NewReader(footerContent), &buf); err != nil {
ctx.ServerError("Render", err)
return nil, nil
}
ctx.Data["footerPresent"] = footerContent != nil
ctx.Data["footerContent"] = buf.String()
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
ctx.Data["CommitCount"] = commitsCount
return wikiRepo, entry
}
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
wikiRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
// get requested pagename
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
if len(pageName) == 0 {
pageName = "Home"
}
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
ctx.Data["RequireHighlightJS"] = true
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
//lookup filename in wiki - get filecontent, gitTree entry , real filename
data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
}
if entry == nil || ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return nil, nil
}
ctx.Data["content"] = string(data)
ctx.Data["sidebarPresent"] = false
ctx.Data["sidebarContent"] = ""
ctx.Data["footerPresent"] = false
ctx.Data["footerContent"] = ""
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("CommitsByFileAndRangeNoFollow", err)
return nil, nil
}
commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)
ctx.Data["Commits"] = commitsHistory
pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
return wikiRepo, entry
}
func renderEditPage(ctx *context.Context) {
wikiRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return
}
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
// get requested pagename
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
if len(pageName) == 0 {
pageName = "Home"
}
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
ctx.Data["RequireHighlightJS"] = true
//lookup filename in wiki - get filecontent, gitTree entry , real filename
data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
}
if entry == nil || ctx.Written() {
return
}
ctx.Data["content"] = string(data)
ctx.Data["sidebarPresent"] = false
ctx.Data["sidebarContent"] = ""
ctx.Data["footerPresent"] = false
ctx.Data["footerContent"] = ""
}
// Wiki renders single wiki page
func Wiki(ctx *context.Context) {
ctx.Data["PageIsWiki"] = true
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
if !ctx.Repo.Repository.HasWiki() {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
wikiRepo, entry := renderViewPage(ctx)
if ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return
}
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
if entry == nil {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
wikiPath := entry.Name()
if markup.Type(wikiPath) != markdown.MarkupName {
ext := strings.ToUpper(filepath.Ext(wikiPath))
ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
}
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
}
ctx.Data["Author"] = lastCommit.Author
ctx.HTML(http.StatusOK, tplWikiView)
}
// WikiRevision renders file revision list of wiki page
func WikiRevision(ctx *context.Context) {
ctx.Data["PageIsWiki"] = true
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
if !ctx.Repo.Repository.HasWiki() {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
wikiRepo, entry := renderRevisionPage(ctx)
if ctx.Written() {
if wikiRepo != nil {
wikiRepo.Close()
}
return
}
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
if entry == nil {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
// Get last change information.
wikiPath := entry.Name()
lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
}
ctx.Data["Author"] = lastCommit.Author
ctx.HTML(http.StatusOK, tplWikiRevision)
}
// WikiPages render wiki pages list page
func WikiPages(ctx *context.Context) {
if !ctx.Repo.Repository.HasWiki() {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
return
}
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
ctx.Data["PageIsWiki"] = true
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
wikiRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
return
}
entries, err := commit.ListEntries()
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("ListEntries", err)
return
}
pages := make([]PageMeta, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
c, err := wikiRepo.GetCommitByPath(entry.Name())
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("GetCommit", err)
return
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
if err != nil {
if models.IsErrWikiInvalidFileName(err) {
continue
}
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("WikiFilenameToName", err)
return
}
pages = append(pages, PageMeta{
Name: wikiName,
SubURL: wiki_service.NameToSubURL(wikiName),
UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
})
}
ctx.Data["Pages"] = pages
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
ctx.HTML(http.StatusOK, tplWikiPages)
}
// WikiRaw outputs raw blob requested by user (image for example)
func WikiRaw(ctx *context.Context) {
wikiRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if wikiRepo != nil {
return
}
}
providedPath := ctx.Params("*")
var entry *git.TreeEntry
if commit != nil {
// Try to find a file with that name
entry, err = findEntryForFile(commit, providedPath)
if err != nil && !git.IsErrNotExist(err) {
ctx.ServerError("findFile", err)
return
}
if entry == nil {
// Try to find a wiki page with that name
if strings.HasSuffix(providedPath, ".md") {
providedPath = providedPath[:len(providedPath)-3]
}
wikiPath := wiki_service.NameToFilename(providedPath)
entry, err = findEntryForFile(commit, wikiPath)
if err != nil && !git.IsErrNotExist(err) {
ctx.ServerError("findFile", err)
return
}
}
}
if entry != nil {
if err = common.ServeBlob(ctx, entry.Blob()); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
}
ctx.NotFound("findEntryForFile", nil)
}
// NewWiki render wiki create page
func NewWiki(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
ctx.Data["PageIsWiki"] = true
ctx.Data["RequireSimpleMDE"] = true
if !ctx.Repo.Repository.HasWiki() {
ctx.Data["title"] = "Home"
}
ctx.HTML(http.StatusOK, tplWikiNew)
}
// NewWikiPost response for wiki create request
func NewWikiPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewWikiForm)
ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
ctx.Data["PageIsWiki"] = true
ctx.Data["RequireSimpleMDE"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplWikiNew)
return
}
if util.IsEmptyString(form.Title) {
ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
return
}
wikiName := wiki_service.NormalizeWikiName(form.Title)
if len(form.Message) == 0 {
form.Message = ctx.Tr("repo.editor.add", form.Title)
}
if err := wiki_service.AddWikiPage(ctx.User, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
if models.IsErrWikiReservedName(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
} else if models.IsErrWikiAlreadyExist(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
} else {
ctx.ServerError("AddWikiPage", err)
}
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName))
}
// EditWiki render wiki modify page
func EditWiki(ctx *context.Context) {
ctx.Data["PageIsWiki"] = true
ctx.Data["PageIsWikiEdit"] = true
ctx.Data["RequireSimpleMDE"] = true
if !ctx.Repo.Repository.HasWiki() {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
return
}
renderEditPage(ctx)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplWikiNew)
}
// EditWikiPost response for wiki modify request
func EditWikiPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewWikiForm)
ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
ctx.Data["PageIsWiki"] = true
ctx.Data["RequireSimpleMDE"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplWikiNew)
return
}
oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
newWikiName := wiki_service.NormalizeWikiName(form.Title)
if len(form.Message) == 0 {
form.Message = ctx.Tr("repo.editor.update", form.Title)
}
if err := wiki_service.EditWikiPage(ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
ctx.ServerError("EditWikiPage", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName))
}
// DeleteWikiPagePost delete wiki page
func DeleteWikiPagePost(ctx *context.Context) {
wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
if len(wikiName) == 0 {
wikiName = "Home"
}
if err := wiki_service.DeleteWikiPage(ctx.User, ctx.Repo.Repository, wikiName); err != nil {
ctx.ServerError("DeleteWikiPage", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/wiki/",
})
}

View File

@@ -0,0 +1,215 @@
// Copyright 2017 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 repo
import (
"io/ioutil"
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
wiki_service "code.gitea.io/gitea/services/wiki"
"github.com/stretchr/testify/assert"
)
const content = "Wiki contents for unit tests"
const message = "Wiki commit message for unit tests"
func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.TreeEntry {
wikiRepo, err := git.OpenRepository(repo.WikiPath())
assert.NoError(t, err)
defer wikiRepo.Close()
commit, err := wikiRepo.GetBranchCommit("master")
assert.NoError(t, err)
entries, err := commit.ListEntries()
assert.NoError(t, err)
for _, entry := range entries {
if entry.Name() == wiki_service.NameToFilename(wikiName) {
return entry
}
}
return nil
}
func wikiContent(t *testing.T, repo *models.Repository, wikiName string) string {
entry := wikiEntry(t, repo, wikiName)
if !assert.NotNil(t, entry) {
return ""
}
reader, err := entry.Blob().DataAsync()
assert.NoError(t, err)
defer reader.Close()
bytes, err := ioutil.ReadAll(reader)
assert.NoError(t, err)
return string(bytes)
}
func assertWikiExists(t *testing.T, repo *models.Repository, wikiName string) {
assert.NotNil(t, wikiEntry(t, repo, wikiName))
}
func assertWikiNotExists(t *testing.T, repo *models.Repository, wikiName string) {
assert.Nil(t, wikiEntry(t, repo, wikiName))
}
func assertPagesMetas(t *testing.T, expectedNames []string, metas interface{}) {
pageMetas, ok := metas.([]PageMeta)
if !assert.True(t, ok) {
return
}
if !assert.Len(t, pageMetas, len(expectedNames)) {
return
}
for i, pageMeta := range pageMetas {
assert.EqualValues(t, expectedNames[i], pageMeta.Name)
}
}
func TestWiki(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
ctx.SetParams(":page", "Home")
test.LoadRepo(t, ctx, 1)
Wiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
}
func TestWikiPages(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
test.LoadRepo(t, ctx, 1)
WikiPages(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name"}, ctx.Data["Pages"])
}
func TestNewWiki(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_new")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
NewWiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
}
func TestNewWikiPost(t *testing.T) {
for _, title := range []string{
"New page",
"&&&&",
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_new")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.NewWikiForm{
Title: title,
Content: content,
Message: message,
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assertWikiExists(t, ctx.Repo.Repository, title)
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
}
}
func TestNewWikiPost_ReservedName(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_new")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.NewWikiForm{
Title: "_edit",
Content: content,
Message: message,
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
func TestEditWiki(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_edit/Home")
ctx.SetParams(":page", "Home")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
EditWiki(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
}
func TestEditWikiPost(t *testing.T) {
for _, title := range []string{
"Home",
"New/<page>",
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_new/Home")
ctx.SetParams(":page", "Home")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.NewWikiForm{
Title: title,
Content: content,
Message: message,
})
EditWikiPost(ctx)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
assertWikiExists(t, ctx.Repo.Repository, title)
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content)
if title != "Home" {
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
}
}
func TestDeleteWikiPagePost(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/Home/delete")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
func TestWikiRaw(t *testing.T) {
for filepath, filetype := range map[string]string{
"jpeg.jpg": "image/jpeg",
"images/jpeg.jpg": "image/jpeg",
"Page With Spaced Name": "text/plain; charset=utf-8",
"Page-With-Spaced-Name": "text/plain; charset=utf-8",
"Page With Spaced Name.md": "text/plain; charset=utf-8",
"Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+filepath)
ctx.SetParams("*", filepath)
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
WikiRaw(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"))
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2020 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 web
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
// tplSwaggerV1Json swagger v1 json template
const tplSwaggerV1Json base.TplName = "swagger/v1_json"
// SwaggerV1Json render swagger v1 json
func SwaggerV1Json(ctx *context.Context) {
t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
ctx.Resp.Header().Set("Content-Type", "application/json")
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
log.Error("%v", err)
ctx.Error(http.StatusInternalServerError)
}
}

1769
routers/web/user/auth.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
// Copyright 2017 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 user
import (
"fmt"
"net/http"
"net/url"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/hcaptcha"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/recaptcha"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/forms"
)
const (
tplSignInOpenID base.TplName = "user/auth/signin_openid"
tplConnectOID base.TplName = "user/auth/signup_openid_connect"
tplSignUpOID base.TplName = "user/auth/signup_openid_register"
)
// SignInOpenID render sign in page
func SignInOpenID(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
if ctx.Query("openid.return_to") != "" {
signInOpenIDVerify(ctx)
return
}
// Check auto-login.
isSucceed, err := AutoSignIn(ctx)
if err != nil {
ctx.ServerError("AutoSignIn", err)
return
}
redirectTo := ctx.Query("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
} else {
redirectTo = ctx.GetCookie("redirect_to")
}
if isSucceed {
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToFirst(redirectTo)
return
}
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLoginOpenID"] = true
ctx.HTML(http.StatusOK, tplSignInOpenID)
}
// Check if the given OpenID URI is allowed by blacklist/whitelist
func allowedOpenIDURI(uri string) (err error) {
// In case a Whitelist is present, URI must be in it
// in order to be accepted
if len(setting.Service.OpenIDWhitelist) != 0 {
for _, pat := range setting.Service.OpenIDWhitelist {
if pat.MatchString(uri) {
return nil // pass
}
}
// must match one of this or be refused
return fmt.Errorf("URI not allowed by whitelist")
}
// A blacklist match expliclty forbids
for _, pat := range setting.Service.OpenIDBlacklist {
if pat.MatchString(uri) {
return fmt.Errorf("URI forbidden by blacklist")
}
}
return nil
}
// SignInOpenIDPost response for openid sign in request
func SignInOpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.SignInOpenIDForm)
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLoginOpenID"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSignInOpenID)
return
}
id, err := openid.Normalize(form.Openid)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
return
}
form.Openid = id
log.Trace("OpenID uri: " + id)
err = allowedOpenIDURI(id)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
return
}
redirectTo := setting.AppURL + "user/login/openid"
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
ctx.RenderWithErr(fmt.Sprintf("Unable to find OpenID provider in %s", redirectTo), tplSignInOpenID, &form)
return
}
// Request optional nickname and email info
// NOTE: change to `openid.sreg.required` to require it
url += "&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1"
url += "&openid.sreg.optional=nickname%2Cemail"
log.Trace("Form-passed openid-remember: %t", form.Remember)
if err := ctx.Session.Set("openid_signin_remember", form.Remember); err != nil {
log.Error("SignInOpenIDPost: Could not set openid_signin_remember in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("SignInOpenIDPost: Unable to save changes to the session: %v", err)
}
ctx.Redirect(url)
}
// signInOpenIDVerify handles response from OpenID provider
func signInOpenIDVerify(ctx *context.Context) {
log.Trace("Incoming call to: " + ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: " + fullURL)
var id, err = openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: " + id)
/* Now we should seek for the user and log him in, or prompt
* to register if not found */
u, err := models.GetUserByOpenID(id)
if err != nil {
if !models.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Error("signInOpenIDVerify: %v", err)
}
if u != nil {
log.Trace("User exists, logging in")
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
handleSignIn(ctx, u, remember)
return
}
log.Trace("User with openid " + id + " does not exist, should connect or register")
parsedURL, err := url.Parse(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
values, err := url.ParseQuery(parsedURL.RawQuery)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
email := values.Get("openid.sreg.email")
nickname := values.Get("openid.sreg.nickname")
log.Trace("User has email=" + email + " and nickname=" + nickname)
if email != "" {
u, err = models.GetUserByEmail(email)
if err != nil {
if !models.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Error("signInOpenIDVerify: %v", err)
}
if u != nil {
log.Trace("Local user " + u.LowerName + " has OpenID provided email " + email)
}
}
if u == nil && nickname != "" {
u, _ = models.GetUserByName(nickname)
if err != nil {
if !models.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
}
if u != nil {
log.Trace("Local user " + u.LowerName + " has OpenID provided nickname " + nickname)
}
}
if err := ctx.Session.Set("openid_verified_uri", id); err != nil {
log.Error("signInOpenIDVerify: Could not set openid_verified_uri in session: %v", err)
}
if err := ctx.Session.Set("openid_determined_email", email); err != nil {
log.Error("signInOpenIDVerify: Could not set openid_determined_email in session: %v", err)
}
if u != nil {
nickname = u.LowerName
}
if err := ctx.Session.Set("openid_determined_username", nickname); err != nil {
log.Error("signInOpenIDVerify: Could not set openid_determined_username in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("signInOpenIDVerify: Unable to save changes to the session: %v", err)
}
if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
ctx.Redirect(setting.AppSubURL + "/user/openid/connect")
} else {
ctx.Redirect(setting.AppSubURL + "/user/openid/register")
}
}
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
func ConnectOpenID(ctx *context.Context) {
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["OpenID"] = oid
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
}
ctx.HTML(http.StatusOK, tplConnectOID)
}
// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
func ConnectOpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ConnectOpenIDForm)
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["OpenID"] = oid
u, err := models.UserSignIn(form.UserName, form.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
} else {
ctx.ServerError("ConnectOpenIDPost", err)
}
return
}
// add OpenID for the user
userOID := &models.UserOpenID{UID: u.ID, URI: oid}
if err = models.AddUserOpenID(userOID); err != nil {
if models.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
handleSignIn(ctx, u, remember)
}
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
func RegisterOpenID(ctx *context.Context) {
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["OpenID"] = oid
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
}
email, _ := ctx.Session.Get("openid_determined_email").(string)
if email != "" {
ctx.Data["email"] = email
}
ctx.HTML(http.StatusOK, tplSignUpOID)
}
// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
func RegisterOpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["OpenID"] = oid
if setting.Service.AllowOnlyInternalRegistration {
ctx.Error(http.StatusForbidden)
return
}
if setting.Service.EnableCaptcha {
var valid bool
var err error
switch setting.Service.CaptchaType {
case setting.ImageCaptcha:
valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
case setting.ReCaptcha:
if err := ctx.Req.ParseForm(); err != nil {
ctx.ServerError("", err)
return
}
valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
case setting.HCaptcha:
if err := ctx.Req.ParseForm(); err != nil {
ctx.ServerError("", err)
return
}
valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
default:
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
return
}
if err != nil {
log.Debug("%s", err.Error())
}
if !valid {
ctx.Data["Err_Captcha"] = true
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form)
return
}
}
length := setting.MinPasswordLength
if length < 256 {
length = 256
}
password, err := util.RandomString(int64(length))
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
return
}
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: password,
IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
}
if !createUserInContext(ctx, tplSignUpOID, form, u, nil, false) {
// error already handled
return
}
// add OpenID for the user
userOID := &models.UserOpenID{UID: u.ID, URI: oid}
if err = models.AddUserOpenID(userOID); err != nil {
if models.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
if !handleUserCreated(ctx, u, nil) {
// error already handled
return
}
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
handleSignIn(ctx, u, remember)
}

View File

@@ -0,0 +1,98 @@
// Copyright 2019 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 user
import (
"errors"
"net/url"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Avatar redirect browser to user avatar of requested size
func Avatar(ctx *context.Context) {
userName := ctx.Params(":username")
size, err := strconv.Atoi(ctx.Params(":size"))
if err != nil {
ctx.ServerError("Invalid avatar size", err)
return
}
log.Debug("Asked avatar for user %v and size %v", userName, size)
var user *models.User
if strings.ToLower(userName) != "ghost" {
user, err = models.GetUserByName(userName)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.ServerError("Requested avatar for invalid user", err)
} else {
ctx.ServerError("Retrieving user by name", err)
}
return
}
} else {
user = models.NewGhostUser()
}
ctx.Redirect(user.RealSizedAvatarLink(size))
}
// AvatarByEmailHash redirects the browser to the appropriate Avatar link
func AvatarByEmailHash(ctx *context.Context) {
var err error
hash := ctx.Params(":hash")
if len(hash) == 0 {
ctx.ServerError("invalid avatar hash", errors.New("hash cannot be empty"))
return
}
var email string
email, err = models.GetEmailForHash(hash)
if err != nil {
ctx.ServerError("invalid avatar hash", err)
return
}
if len(email) == 0 {
ctx.Redirect(models.DefaultAvatarLink())
return
}
size := ctx.QueryInt("size")
if size == 0 {
size = models.DefaultAvatarSize
}
var avatarURL *url.URL
if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
avatarURL, err = models.LibravatarURL(email)
if err != nil {
avatarURL, err = url.Parse(models.DefaultAvatarLink())
if err != nil {
ctx.ServerError("invalid default avatar url", err)
return
}
}
} else if !setting.DisableGravatar {
copyOfGravatarSourceURL := *setting.GravatarSourceURL
avatarURL = &copyOfGravatarSourceURL
avatarURL.Path = path.Join(avatarURL.Path, hash)
} else {
avatarURL, err = url.Parse(models.DefaultAvatarLink())
if err != nil {
ctx.ServerError("invalid default avatar url", err)
return
}
}
ctx.Redirect(models.MakeFinalAvatarURL(avatarURL, size))
}

913
routers/web/user/home.go Normal file
View File

@@ -0,0 +1,913 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 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 user
import (
"bytes"
"fmt"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
jsoniter "github.com/json-iterator/go"
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/armor"
"xorm.io/builder"
)
const (
tplDashboard base.TplName = "user/dashboard/dashboard"
tplIssues base.TplName = "user/dashboard/issues"
tplMilestones base.TplName = "user/dashboard/milestones"
tplProfile base.TplName = "user/profile"
)
// getDashboardContextUser finds out which context user dashboard is being viewed as .
func getDashboardContextUser(ctx *context.Context) *models.User {
ctxUser := ctx.User
orgName := ctx.Params(":org")
if len(orgName) > 0 {
ctxUser = ctx.Org.Organization
ctx.Data["Teams"] = ctx.Org.Organization.Teams
}
ctx.Data["ContextUser"] = ctxUser
if err := ctx.User.GetOrganizations(&models.SearchOrganizationsOptions{All: true}); err != nil {
ctx.ServerError("GetOrganizations", err)
return nil
}
ctx.Data["Orgs"] = ctx.User.Orgs
return ctxUser
}
// retrieveFeeds loads feeds for the specified user
func retrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) {
actions, err := models.GetFeeds(options)
if err != nil {
ctx.ServerError("GetFeeds", err)
return
}
userCache := map[int64]*models.User{options.RequestedUser.ID: options.RequestedUser}
if ctx.User != nil {
userCache[ctx.User.ID] = ctx.User
}
for _, act := range actions {
if act.ActUser != nil {
userCache[act.ActUserID] = act.ActUser
}
}
for _, act := range actions {
repoOwner, ok := userCache[act.Repo.OwnerID]
if !ok {
repoOwner, err = models.GetUserByID(act.Repo.OwnerID)
if err != nil {
if models.IsErrUserNotExist(err) {
continue
}
ctx.ServerError("GetUserByID", err)
return
}
userCache[repoOwner.ID] = repoOwner
}
act.Repo.Owner = repoOwner
}
ctx.Data["Feeds"] = actions
}
// Dashboard render the dashboard page
func Dashboard(ctx *context.Context) {
ctxUser := getDashboardContextUser(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
ctx.Data["PageIsDashboard"] = true
ctx.Data["PageIsNews"] = true
ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
if setting.Service.EnableUserHeatmap {
data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User)
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
return
}
ctx.Data["HeatmapData"] = data
}
var err error
var mirrors []*models.Repository
if ctxUser.IsOrganization() {
var env models.AccessibleReposEnvironment
if ctx.Org.Team != nil {
env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team)
} else {
env, err = ctxUser.AccessibleReposEnv(ctx.User.ID)
if err != nil {
ctx.ServerError("AccessibleReposEnv", err)
return
}
}
mirrors, err = env.MirrorRepos()
if err != nil {
ctx.ServerError("env.MirrorRepos", err)
return
}
} else {
mirrors, err = ctxUser.GetMirrorRepositories()
if err != nil {
ctx.ServerError("GetMirrorRepositories", err)
return
}
}
ctx.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum
if err := models.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil {
ctx.ServerError("MirrorRepositoryList.LoadAttributes", err)
return
}
ctx.Data["MirrorCount"] = len(mirrors)
ctx.Data["Mirrors"] = mirrors
retrieveFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team,
Actor: ctx.User,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: false,
Date: ctx.Query("date"),
})
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplDashboard)
}
// Milestones render the user milestones page
func Milestones(ctx *context.Context) {
if models.UnitTypeIssues.UnitGlobalDisabled() && models.UnitTypePullRequests.UnitGlobalDisabled() {
log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled")
ctx.Status(404)
return
}
ctx.Data["Title"] = ctx.Tr("milestones")
ctx.Data["PageIsMilestonesDashboard"] = true
ctxUser := getDashboardContextUser(ctx)
if ctx.Written() {
return
}
repoOpts := models.SearchRepoOptions{
Actor: ctxUser,
OwnerID: ctxUser.ID,
Private: true,
AllPublic: false, // Include also all public repositories of users and public organisations
AllLimited: false, // Include also all public repositories of limited organisations
HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
}
if ctxUser.IsOrganization() && ctx.Org.Team != nil {
repoOpts.TeamID = ctx.Org.Team.ID
}
var (
userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
repoCond = userRepoCond
repoIDs []int64
reposQuery = ctx.Query("repos")
isShowClosed = ctx.Query("state") == "closed"
sortType = ctx.Query("sort")
page = ctx.QueryInt("page")
keyword = strings.Trim(ctx.Query("q"), " ")
)
if page <= 1 {
page = 1
}
if len(reposQuery) != 0 {
if issueReposQueryPattern.MatchString(reposQuery) {
// remove "[" and "]" from string
reposQuery = reposQuery[1 : len(reposQuery)-1]
//for each ID (delimiter ",") add to int to repoIDs
for _, rID := range strings.Split(reposQuery, ",") {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64, err := strconv.ParseInt(rID, 10, 64)
// If the repo id specified by query is not parseable or not accessible by user, just ignore it.
if err == nil {
repoIDs = append(repoIDs, rIDint64)
}
}
}
if len(repoIDs) > 0 {
// Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs
// But the original repoCond has a limitation
repoCond = repoCond.And(builder.In("id", repoIDs))
}
} else {
log.Warn("issueReposQueryPattern not match with query")
}
}
counts, err := models.CountMilestonesByRepoCondAndKw(userRepoCond, keyword, isShowClosed)
if err != nil {
ctx.ServerError("CountMilestonesByRepoIDs", err)
return
}
milestones, err := models.SearchMilestones(repoCond, page, isShowClosed, sortType, keyword)
if err != nil {
ctx.ServerError("SearchMilestones", err)
return
}
showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false)
if err != nil {
ctx.ServerError("SearchRepositoryByCondition", err)
return
}
sort.Sort(showRepos)
for i := 0; i < len(milestones); {
for _, repo := range showRepos {
if milestones[i].RepoID == repo.ID {
milestones[i].Repo = repo
break
}
}
if milestones[i].Repo == nil {
log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID)
milestones = append(milestones[:i], milestones[i+1:]...)
continue
}
milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: milestones[i].Repo.Link(),
Metas: milestones[i].Repo.ComposeMetas(),
}, milestones[i].Content)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
if milestones[i].Repo.IsTimetrackerEnabled() {
err := milestones[i].LoadTotalTrackedTime()
if err != nil {
ctx.ServerError("LoadTotalTrackedTime", err)
return
}
}
i++
}
milestoneStats, err := models.GetMilestonesStatsByRepoCondAndKw(repoCond, keyword)
if err != nil {
ctx.ServerError("GetMilestoneStats", err)
return
}
var totalMilestoneStats *models.MilestonesStats
if len(repoIDs) == 0 {
totalMilestoneStats = milestoneStats
} else {
totalMilestoneStats, err = models.GetMilestonesStatsByRepoCondAndKw(userRepoCond, keyword)
if err != nil {
ctx.ServerError("GetMilestoneStats", err)
return
}
}
var pagerCount int
if isShowClosed {
ctx.Data["State"] = "closed"
ctx.Data["Total"] = totalMilestoneStats.ClosedCount
pagerCount = int(milestoneStats.ClosedCount)
} else {
ctx.Data["State"] = "open"
ctx.Data["Total"] = totalMilestoneStats.OpenCount
pagerCount = int(milestoneStats.OpenCount)
}
ctx.Data["Milestones"] = milestones
ctx.Data["Repos"] = showRepos
ctx.Data["Counts"] = counts
ctx.Data["MilestoneStats"] = milestoneStats
ctx.Data["SortType"] = sortType
ctx.Data["Keyword"] = keyword
if milestoneStats.Total() != totalMilestoneStats.Total() {
ctx.Data["RepoIDs"] = repoIDs
}
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "repos", "RepoIDs")
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestones)
}
// Pulls renders the user's pull request overview page
func Pulls(ctx *context.Context) {
if models.UnitTypePullRequests.UnitGlobalDisabled() {
log.Debug("Pull request overview page not available as it is globally disabled.")
ctx.Status(404)
return
}
ctx.Data["Title"] = ctx.Tr("pull_requests")
ctx.Data["PageIsPulls"] = true
buildIssueOverview(ctx, models.UnitTypePullRequests)
}
// Issues renders the user's issues overview page
func Issues(ctx *context.Context) {
if models.UnitTypeIssues.UnitGlobalDisabled() {
log.Debug("Issues overview page not available as it is globally disabled.")
ctx.Status(404)
return
}
ctx.Data["Title"] = ctx.Tr("issues")
ctx.Data["PageIsIssues"] = true
buildIssueOverview(ctx, models.UnitTypeIssues)
}
// Regexp for repos query
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
func buildIssueOverview(ctx *context.Context, unitType models.UnitType) {
// ----------------------------------------------------
// Determine user; can be either user or organization.
// Return with NotFound or ServerError if unsuccessful.
// ----------------------------------------------------
ctxUser := getDashboardContextUser(ctx)
if ctx.Written() {
return
}
var (
viewType string
sortType = ctx.Query("sort")
filterMode = models.FilterModeAll
)
// --------------------------------------------------------------------------------
// Distinguish User from Organization.
// Org:
// - Remember pre-determined viewType string for later. Will be posted to ctx.Data.
// Organization does not have view type and filter mode.
// User:
// - Use ctx.Query("type") to determine filterMode.
// The type is set when clicking for example "assigned to me" on the overview page.
// - Remember either this or a fallback. Will be posted to ctx.Data.
// --------------------------------------------------------------------------------
// TODO: distinguish during routing
viewType = ctx.Query("type")
switch viewType {
case "assigned":
filterMode = models.FilterModeAssign
case "created_by":
filterMode = models.FilterModeCreate
case "mentioned":
filterMode = models.FilterModeMention
case "review_requested":
filterMode = models.FilterModeReviewRequested
case "your_repositories": // filterMode already set to All
default:
viewType = "your_repositories"
}
// --------------------------------------------------------------------------
// Build opts (IssuesOptions), which contains filter information.
// Will eventually be used to retrieve issues relevant for the overview page.
// Note: Non-final states of opts are used in-between, namely for:
// - Keyword search
// - Count Issues by repo
// --------------------------------------------------------------------------
isPullList := unitType == models.UnitTypePullRequests
opts := &models.IssuesOptions{
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
IsArchived: util.OptionalBoolFalse,
}
// Get repository IDs where User/Org/Team has access.
var team *models.Team
if ctx.Org != nil {
team = ctx.Org.Team
}
userRepoIDs, err := getActiveUserRepoIDs(ctxUser, team, unitType)
if err != nil {
ctx.ServerError("userRepoIDs", err)
return
}
switch filterMode {
case models.FilterModeAll:
opts.RepoIDs = userRepoIDs
case models.FilterModeAssign:
opts.AssigneeID = ctx.User.ID
case models.FilterModeCreate:
opts.PosterID = ctx.User.ID
case models.FilterModeMention:
opts.MentionedID = ctx.User.ID
case models.FilterModeReviewRequested:
opts.ReviewRequestedID = ctx.User.ID
}
if ctxUser.IsOrganization() {
opts.RepoIDs = userRepoIDs
}
// keyword holds the search term entered into the search field.
keyword := strings.Trim(ctx.Query("q"), " ")
ctx.Data["Keyword"] = keyword
// Execute keyword search for issues.
// USING NON-FINAL STATE OF opts FOR A QUERY.
issueIDsFromSearch, err := issueIDsFromSearch(ctxUser, keyword, opts)
if err != nil {
ctx.ServerError("issueIDsFromSearch", err)
return
}
// Ensure no issues are returned if a keyword was provided that didn't match any issues.
var forceEmpty bool
if len(issueIDsFromSearch) > 0 {
opts.IssueIDs = issueIDsFromSearch
} else if len(keyword) > 0 {
forceEmpty = true
}
// Educated guess: Do or don't show closed issues.
isShowClosed := ctx.Query("state") == "closed"
opts.IsClosed = util.OptionalBoolOf(isShowClosed)
// Filter repos and count issues in them. Count will be used later.
// USING NON-FINAL STATE OF opts FOR A QUERY.
var issueCountByRepo map[int64]int64
if !forceEmpty {
issueCountByRepo, err = models.CountIssuesByRepo(opts)
if err != nil {
ctx.ServerError("CountIssuesByRepo", err)
return
}
}
// Make sure page number is at least 1. Will be posted to ctx.Data.
page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
opts.Page = page
opts.PageSize = setting.UI.IssuePagingNum
// Get IDs for labels (a filter option for issues/pulls).
// Required for IssuesOptions.
var labelIDs []int64
selectedLabels := ctx.Query("labels")
if len(selectedLabels) > 0 && selectedLabels != "0" {
labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
if err != nil {
ctx.ServerError("StringsToInt64s", err)
return
}
}
opts.LabelIDs = labelIDs
// Parse ctx.Query("repos") and remember matched repo IDs for later.
// Gets set when clicking filters on the issues overview page.
repoIDs := getRepoIDs(ctx.Query("repos"))
if len(repoIDs) > 0 {
opts.RepoIDs = repoIDs
}
// ------------------------------
// Get issues as defined by opts.
// ------------------------------
// Slice of Issues that will be displayed on the overview page
// USING FINAL STATE OF opts FOR A QUERY.
var issues []*models.Issue
if !forceEmpty {
issues, err = models.Issues(opts)
if err != nil {
ctx.ServerError("Issues", err)
return
}
} else {
issues = []*models.Issue{}
}
// ----------------------------------
// Add repository pointers to Issues.
// ----------------------------------
// showReposMap maps repository IDs to their Repository pointers.
showReposMap, err := repoIDMap(ctxUser, issueCountByRepo, unitType)
if err != nil {
if models.IsErrRepoNotExist(err) {
ctx.NotFound("GetRepositoryByID", err)
return
}
ctx.ServerError("repoIDMap", err)
return
}
// a RepositoryList
showRepos := models.RepositoryListOfMap(showReposMap)
sort.Sort(showRepos)
if err = showRepos.LoadAttributes(); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
// maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
for _, issue := range issues {
issue.Repo = showReposMap[issue.RepoID]
}
commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
if err != nil {
ctx.ServerError("GetIssuesLastCommitStatus", err)
return
}
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
userIssueStatsOpts := models.UserIssueStatsOptions{
UserID: ctx.User.ID,
UserRepoIDs: userRepoIDs,
FilterMode: filterMode,
IsPull: isPullList,
IsClosed: isShowClosed,
IsArchived: util.OptionalBoolFalse,
LabelIDs: opts.LabelIDs,
}
if len(repoIDs) > 0 {
userIssueStatsOpts.UserRepoIDs = repoIDs
}
if ctxUser.IsOrganization() {
userIssueStatsOpts.RepoIDs = userRepoIDs
}
userIssueStats, err := models.GetUserIssueStats(userIssueStatsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats User", err)
return
}
var shownIssueStats *models.IssueStats
if !forceEmpty {
statsOpts := models.UserIssueStatsOptions{
UserID: ctx.User.ID,
UserRepoIDs: userRepoIDs,
FilterMode: filterMode,
IsPull: isPullList,
IsClosed: isShowClosed,
IssueIDs: issueIDsFromSearch,
IsArchived: util.OptionalBoolFalse,
LabelIDs: opts.LabelIDs,
}
if len(repoIDs) > 0 {
statsOpts.RepoIDs = repoIDs
} else if ctxUser.IsOrganization() {
statsOpts.RepoIDs = userRepoIDs
}
shownIssueStats, err = models.GetUserIssueStats(statsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats Shown", err)
return
}
} else {
shownIssueStats = &models.IssueStats{}
}
var allIssueStats *models.IssueStats
if !forceEmpty {
allIssueStatsOpts := models.UserIssueStatsOptions{
UserID: ctx.User.ID,
UserRepoIDs: userRepoIDs,
FilterMode: filterMode,
IsPull: isPullList,
IsClosed: isShowClosed,
IssueIDs: issueIDsFromSearch,
IsArchived: util.OptionalBoolFalse,
LabelIDs: opts.LabelIDs,
}
if ctxUser.IsOrganization() {
allIssueStatsOpts.RepoIDs = userRepoIDs
}
allIssueStats, err = models.GetUserIssueStats(allIssueStatsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats All", err)
return
}
} else {
allIssueStats = &models.IssueStats{}
}
// Will be posted to ctx.Data.
var shownIssues int
if !isShowClosed {
shownIssues = int(shownIssueStats.OpenCount)
ctx.Data["TotalIssueCount"] = int(allIssueStats.OpenCount)
} else {
shownIssues = int(shownIssueStats.ClosedCount)
ctx.Data["TotalIssueCount"] = int(allIssueStats.ClosedCount)
}
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
issue_service.GetRefEndNamesAndURLs(issues, ctx.Query("RepoLink"))
ctx.Data["Issues"] = issues
approvalCounts, err := models.IssueList(issues).GetApprovalCounts()
if err != nil {
ctx.ServerError("ApprovalCounts", err)
return
}
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
counts, ok := approvalCounts[issueID]
if !ok || len(counts) == 0 {
return 0
}
reviewTyp := models.ReviewTypeApprove
if typ == "reject" {
reviewTyp = models.ReviewTypeReject
} else if typ == "waiting" {
reviewTyp = models.ReviewTypeRequest
}
for _, count := range counts {
if count.Type == reviewTyp {
return count.Count
}
}
return 0
}
ctx.Data["CommitStatus"] = commitStatus
ctx.Data["Repos"] = showRepos
ctx.Data["Counts"] = issueCountByRepo
ctx.Data["IssueStats"] = userIssueStats
ctx.Data["ShownIssueStats"] = shownIssueStats
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["RepoIDs"] = repoIDs
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels
if isShowClosed {
ctx.Data["State"] = "closed"
} else {
ctx.Data["State"] = "open"
}
// Convert []int64 to string
json := jsoniter.ConfigCompatibleWithStandardLibrary
reposParam, _ := json.Marshal(repoIDs)
ctx.Data["ReposParam"] = string(reposParam)
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "type", "ViewType")
pager.AddParam(ctx, "repos", "ReposParam")
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
pager.AddParam(ctx, "labels", "SelectLabels")
pager.AddParam(ctx, "milestone", "MilestoneID")
pager.AddParam(ctx, "assignee", "AssigneeID")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues)
}
func getRepoIDs(reposQuery string) []int64 {
if len(reposQuery) == 0 || reposQuery == "[]" {
return []int64{}
}
if !issueReposQueryPattern.MatchString(reposQuery) {
log.Warn("issueReposQueryPattern does not match query")
return []int64{}
}
var repoIDs []int64
// remove "[" and "]" from string
reposQuery = reposQuery[1 : len(reposQuery)-1]
//for each ID (delimiter ",") add to int to repoIDs
for _, rID := range strings.Split(reposQuery, ",") {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64, err := strconv.ParseInt(rID, 10, 64)
if err == nil {
repoIDs = append(repoIDs, rIDint64)
}
}
}
return repoIDs
}
func getActiveUserRepoIDs(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
var userRepoIDs []int64
var err error
if ctxUser.IsOrganization() {
userRepoIDs, err = getActiveTeamOrOrgRepoIds(ctxUser, team, unitType)
if err != nil {
return nil, fmt.Errorf("orgRepoIds: %v", err)
}
} else {
userRepoIDs, err = ctxUser.GetActiveAccessRepoIDs(unitType)
if err != nil {
return nil, fmt.Errorf("ctxUser.GetAccessRepoIDs: %v", err)
}
}
if len(userRepoIDs) == 0 {
userRepoIDs = []int64{-1}
}
return userRepoIDs, nil
}
// getActiveTeamOrOrgRepoIds gets RepoIDs for ctxUser as Organization.
// Should be called if and only if ctxUser.IsOrganization == true.
func getActiveTeamOrOrgRepoIds(ctxUser *models.User, team *models.Team, unitType models.UnitType) ([]int64, error) {
var orgRepoIDs []int64
var err error
var env models.AccessibleReposEnvironment
if team != nil {
env = ctxUser.AccessibleTeamReposEnv(team)
} else {
env, err = ctxUser.AccessibleReposEnv(ctxUser.ID)
if err != nil {
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
}
}
orgRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
if err != nil {
return nil, fmt.Errorf("env.RepoIDs: %v", err)
}
orgRepoIDs, err = models.FilterOutRepoIdsWithoutUnitAccess(ctxUser, orgRepoIDs, unitType)
if err != nil {
return nil, fmt.Errorf("FilterOutRepoIdsWithoutUnitAccess: %v", err)
}
return orgRepoIDs, nil
}
func issueIDsFromSearch(ctxUser *models.User, keyword string, opts *models.IssuesOptions) ([]int64, error) {
if len(keyword) == 0 {
return []int64{}, nil
}
searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
if err != nil {
return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err)
}
issueIDsFromSearch, err := issue_indexer.SearchIssuesByKeyword(searchRepoIDs, keyword)
if err != nil {
return nil, fmt.Errorf("SearchIssuesByKeyword: %v", err)
}
return issueIDsFromSearch, nil
}
func repoIDMap(ctxUser *models.User, issueCountByRepo map[int64]int64, unitType models.UnitType) (map[int64]*models.Repository, error) {
repoByID := make(map[int64]*models.Repository, len(issueCountByRepo))
for id := range issueCountByRepo {
if id <= 0 {
continue
}
if _, ok := repoByID[id]; !ok {
repo, err := models.GetRepositoryByID(id)
if models.IsErrRepoNotExist(err) {
return nil, err
} else if err != nil {
return nil, fmt.Errorf("GetRepositoryByID: [%d]%v", id, err)
}
repoByID[id] = repo
}
repo := repoByID[id]
// Check if user has access to given repository.
perm, err := models.GetUserRepoPermission(repo, ctxUser)
if err != nil {
return nil, fmt.Errorf("GetUserRepoPermission: [%d]%v", id, err)
}
if !perm.CanRead(unitType) {
log.Debug("User created Issues in Repository which they no longer have access to: [%d]", id)
}
}
return repoByID, nil
}
// ShowSSHKeys output all the ssh keys of user by uid
func ShowSSHKeys(ctx *context.Context, uid int64) {
keys, err := models.ListPublicKeys(uid, models.ListOptions{})
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
var buf bytes.Buffer
for i := range keys {
buf.WriteString(keys[i].OmitEmail())
buf.WriteString("\n")
}
ctx.PlainText(200, buf.Bytes())
}
// ShowGPGKeys output all the public GPG keys of user by uid
func ShowGPGKeys(ctx *context.Context, uid int64) {
keys, err := models.ListGPGKeys(uid, models.ListOptions{})
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
entities := make([]*openpgp.Entity, 0)
failedEntitiesID := make([]string, 0)
for _, k := range keys {
e, err := models.GPGKeyToEntity(k)
if err != nil {
if models.IsErrGPGKeyImportNotExist(err) {
failedEntitiesID = append(failedEntitiesID, k.KeyID)
continue //Skip previous import without backup of imported armored key
}
ctx.ServerError("ShowGPGKeys", err)
return
}
entities = append(entities, e)
}
var buf bytes.Buffer
headers := make(map[string]string)
if len(failedEntitiesID) > 0 { //If some key need re-import to be exported
headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
}
writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers)
for _, e := range entities {
err = e.Serialize(writer) //TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
if err != nil {
ctx.ServerError("ShowGPGKeys", err)
return
}
}
writer.Close()
ctx.PlainText(200, buf.Bytes())
}
// Email2User show user page via email
func Email2User(ctx *context.Context) {
u, err := models.GetUserByEmail(ctx.Query("email"))
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.NotFound("GetUserByEmail", err)
} else {
ctx.ServerError("GetUserByEmail", err)
}
return
}
ctx.Redirect(setting.AppSubURL + "/user/" + u.Name)
}

View File

@@ -0,0 +1,118 @@
// Copyright 2017 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 user
import (
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestArchivedIssues(t *testing.T) {
// Arrange
setting.UI.IssuePagingNum = 1
assert.NoError(t, models.LoadFixtures())
ctx := test.MockContext(t, "issues")
test.LoadUser(t, ctx, 30)
ctx.Req.Form.Set("state", "open")
// Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctx.User})
assert.Len(t, repos, 2)
IsArchived := make(map[int64]bool)
NumIssues := make(map[int64]int)
for _, repo := range repos {
IsArchived[repo.ID] = repo.IsArchived
NumIssues[repo.ID] = repo.NumIssues
}
assert.False(t, IsArchived[50])
assert.EqualValues(t, 1, NumIssues[50])
assert.True(t, IsArchived[51])
assert.EqualValues(t, 1, NumIssues[51])
// Act
Issues(ctx)
// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{50: 1}, ctx.Data["Counts"])
assert.Len(t, ctx.Data["Issues"], 1)
assert.Len(t, ctx.Data["Repos"], 1)
}
func TestIssues(t *testing.T) {
setting.UI.IssuePagingNum = 1
assert.NoError(t, models.LoadFixtures())
ctx := test.MockContext(t, "issues")
test.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "closed")
Issues(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{1: 1, 2: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.Len(t, ctx.Data["Issues"], 1)
assert.Len(t, ctx.Data["Repos"], 2)
}
func TestPulls(t *testing.T) {
setting.UI.IssuePagingNum = 20
assert.NoError(t, models.LoadFixtures())
ctx := test.MockContext(t, "pulls")
test.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "open")
Pulls(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.Len(t, ctx.Data["Issues"], 3)
}
func TestMilestones(t *testing.T) {
setting.UI.IssuePagingNum = 1
assert.NoError(t, models.LoadFixtures())
ctx := test.MockContext(t, "milestones")
test.LoadUser(t, ctx, 2)
ctx.SetParams("sort", "issues")
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
assert.EqualValues(t, 1, ctx.Data["Total"])
assert.Len(t, ctx.Data["Milestones"], 1)
assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
}
func TestMilestonesForSpecificRepo(t *testing.T) {
setting.UI.IssuePagingNum = 1
assert.NoError(t, models.LoadFixtures())
ctx := test.MockContext(t, "milestones")
test.LoadUser(t, ctx, 2)
ctx.SetParams("sort", "issues")
ctx.SetParams("repo", "1")
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
assert.EqualValues(t, 1, ctx.Data["Total"])
assert.Len(t, ctx.Data["Milestones"], 1)
assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
}

View File

@@ -0,0 +1,16 @@
// Copyright 2017 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 user
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", "..", ".."))
}

View File

@@ -0,0 +1,192 @@
// Copyright 2019 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 user
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
const (
tplNotification base.TplName = "user/notification/notification"
tplNotificationDiv base.TplName = "user/notification/notification_div"
)
// GetNotificationCount is the middleware that sets the notification count in the context
func GetNotificationCount(c *context.Context) {
if strings.HasPrefix(c.Req.URL.Path, "/api") {
return
}
if !c.IsSigned {
return
}
c.Data["NotificationUnreadCount"] = func() int64 {
count, err := models.GetNotificationCount(c.User, models.NotificationStatusUnread)
if err != nil {
c.ServerError("GetNotificationCount", err)
return -1
}
return count
}
}
// Notifications is the notifications page
func Notifications(c *context.Context) {
getNotifications(c)
if c.Written() {
return
}
if c.QueryBool("div-only") {
c.HTML(http.StatusOK, tplNotificationDiv)
return
}
c.HTML(http.StatusOK, tplNotification)
}
func getNotifications(c *context.Context) {
var (
keyword = strings.Trim(c.Query("q"), " ")
status models.NotificationStatus
page = c.QueryInt("page")
perPage = c.QueryInt("perPage")
)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
switch keyword {
case "read":
status = models.NotificationStatusRead
default:
status = models.NotificationStatusUnread
}
total, err := models.GetNotificationCount(c.User, status)
if err != nil {
c.ServerError("ErrGetNotificationCount", err)
return
}
// redirect to last page if request page is more than total pages
pager := context.NewPagination(int(total), perPage, page, 5)
if pager.Paginater.Current() < page {
c.Redirect(fmt.Sprintf("/notifications?q=%s&page=%d", c.Query("q"), pager.Paginater.Current()))
return
}
statuses := []models.NotificationStatus{status, models.NotificationStatusPinned}
notifications, err := models.NotificationsForUser(c.User, statuses, page, perPage)
if err != nil {
c.ServerError("ErrNotificationsForUser", err)
return
}
failCount := 0
repos, failures, err := notifications.LoadRepos()
if err != nil {
c.ServerError("LoadRepos", err)
return
}
notifications = notifications.Without(failures)
if err := repos.LoadAttributes(); err != nil {
c.ServerError("LoadAttributes", err)
return
}
failCount += len(failures)
failures, err = notifications.LoadIssues()
if err != nil {
c.ServerError("LoadIssues", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
failures, err = notifications.LoadComments()
if err != nil {
c.ServerError("LoadComments", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
if failCount > 0 {
c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
}
c.Data["Title"] = c.Tr("notifications")
c.Data["Keyword"] = keyword
c.Data["Status"] = status
c.Data["Notifications"] = notifications
pager.SetDefaultParams(c)
c.Data["Page"] = pager
}
// NotificationStatusPost is a route for changing the status of a notification
func NotificationStatusPost(c *context.Context) {
var (
notificationID, _ = strconv.ParseInt(c.Req.PostFormValue("notification_id"), 10, 64)
statusStr = c.Req.PostFormValue("status")
status models.NotificationStatus
)
switch statusStr {
case "read":
status = models.NotificationStatusRead
case "unread":
status = models.NotificationStatusUnread
case "pinned":
status = models.NotificationStatusPinned
default:
c.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
return
}
if err := models.SetNotificationStatus(notificationID, c.User, status); err != nil {
c.ServerError("SetNotificationStatus", err)
return
}
if !c.QueryBool("noredirect") {
url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, c.Query("page"))
c.Redirect(url, http.StatusSeeOther)
}
getNotifications(c)
if c.Written() {
return
}
c.Data["Link"] = setting.AppURL + "notifications"
c.HTML(http.StatusOK, tplNotificationDiv)
}
// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
func NotificationPurgePost(c *context.Context) {
err := models.UpdateNotificationStatuses(c.User, models.NotificationStatusUnread, models.NotificationStatusRead)
if err != nil {
c.ServerError("ErrUpdateNotificationStatuses", err)
return
}
url := fmt.Sprintf("%s/notifications", setting.AppSubURL)
c.Redirect(url, http.StatusSeeOther)
}

646
routers/web/user/oauth.go Normal file
View File

@@ -0,0 +1,646 @@
// Copyright 2019 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 user
import (
"encoding/base64"
"fmt"
"html"
"net/http"
"net/url"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/binding"
"github.com/dgrijalva/jwt-go"
)
const (
tplGrantAccess base.TplName = "user/auth/grant"
tplGrantError base.TplName = "user/auth/grant_error"
)
// TODO move error and responses to SDK or models
// AuthorizeErrorCode represents an error code specified in RFC 6749
type AuthorizeErrorCode string
const (
// ErrorCodeInvalidRequest represents the according error in RFC 6749
ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
// ErrorCodeUnauthorizedClient represents the according error in RFC 6749
ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
// ErrorCodeAccessDenied represents the according error in RFC 6749
ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
// ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
// ErrorCodeInvalidScope represents the according error in RFC 6749
ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
// ErrorCodeServerError represents the according error in RFC 6749
ErrorCodeServerError AuthorizeErrorCode = "server_error"
// ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
)
// AuthorizeError represents an error type specified in RFC 6749
type AuthorizeError struct {
ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
ErrorDescription string
State string
}
// Error returns the error message
func (err AuthorizeError) Error() string {
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
}
// AccessTokenErrorCode represents an error code specified in RFC 6749
type AccessTokenErrorCode string
const (
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidClient = "invalid_client"
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidScope = "invalid_scope"
)
// AccessTokenError represents an error response specified in RFC 6749
type AccessTokenError struct {
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
ErrorDescription string `json:"error_description"`
}
// Error returns the error message
func (err AccessTokenError) Error() string {
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
}
// BearerTokenErrorCode represents an error code specified in RFC 6750
type BearerTokenErrorCode string
const (
// BearerTokenErrorCodeInvalidRequest represents an error code specified in RFC 6750
BearerTokenErrorCodeInvalidRequest BearerTokenErrorCode = "invalid_request"
// BearerTokenErrorCodeInvalidToken represents an error code specified in RFC 6750
BearerTokenErrorCodeInvalidToken BearerTokenErrorCode = "invalid_token"
// BearerTokenErrorCodeInsufficientScope represents an error code specified in RFC 6750
BearerTokenErrorCodeInsufficientScope BearerTokenErrorCode = "insufficient_scope"
)
// BearerTokenError represents an error response specified in RFC 6750
type BearerTokenError struct {
ErrorCode BearerTokenErrorCode `json:"error" form:"error"`
ErrorDescription string `json:"error_description"`
}
// TokenType specifies the kind of token
type TokenType string
const (
// TokenTypeBearer represents a token type specified in RFC 6749
TokenTypeBearer TokenType = "bearer"
// TokenTypeMAC represents a token type specified in RFC 6749
TokenTypeMAC = "mac"
)
// AccessTokenResponse represents a successful access token response
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType TokenType `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
}
func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(); err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "cannot increase the grant counter",
}
}
}
// generate access token to access the API
expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
accessToken := &models.OAuth2Token{
GrantID: grant.ID,
Type: models.TypeAccessToken,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationDate.AsTime().Unix(),
},
}
signedAccessToken, err := accessToken.SignToken()
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
// generate refresh token to request an access token after it expired later
refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
refreshToken := &models.OAuth2Token{
GrantID: grant.ID,
Counter: grant.Counter,
Type: models.TypeRefreshToken,
StandardClaims: jwt.StandardClaims{
ExpiresAt: refreshExpirationDate,
},
}
signedRefreshToken, err := refreshToken.SignToken()
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
// generate OpenID Connect id_token
signedIDToken := ""
if grant.ScopeContains("openid") {
app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot find application",
}
}
idToken := &models.OIDCToken{
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationDate.AsTime().Unix(),
Issuer: setting.AppURL,
Audience: app.ClientID,
Subject: fmt.Sprint(grant.UserID),
},
Nonce: grant.Nonce,
}
signedIDToken, err = idToken.SignToken(clientSecret)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
}
return &AccessTokenResponse{
AccessToken: signedAccessToken,
TokenType: TokenTypeBearer,
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
RefreshToken: signedRefreshToken,
IDToken: signedIDToken,
}, nil
}
type userInfoResponse struct {
Sub string `json:"sub"`
Name string `json:"name"`
Username string `json:"preferred_username"`
Email string `json:"email"`
Picture string `json:"picture"`
}
// InfoOAuth manages request for userinfo endpoint
func InfoOAuth(ctx *context.Context) {
header := ctx.Req.Header.Get("Authorization")
auths := strings.Fields(header)
if len(auths) != 2 || auths[0] != "Bearer" {
ctx.HandleText(http.StatusUnauthorized, "no valid auth token authorization")
return
}
uid := sso.CheckOAuthAccessToken(auths[1])
if uid == 0 {
handleBearerTokenError(ctx, BearerTokenError{
ErrorCode: BearerTokenErrorCodeInvalidToken,
ErrorDescription: "Access token not assigned to any user",
})
return
}
authUser, err := models.GetUserByID(uid)
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
response := &userInfoResponse{
Sub: fmt.Sprint(authUser.ID),
Name: authUser.FullName,
Username: authUser.Name,
Email: authUser.Email,
Picture: authUser.AvatarLink(),
}
ctx.JSON(http.StatusOK, response)
}
// AuthorizeOAuth manages authorize requests
func AuthorizeOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AuthorizationForm)
errs := binding.Errors{}
errs = form.Validate(ctx.Req, errs)
if len(errs) > 0 {
errstring := ""
for _, e := range errs {
errstring += e.Error() + "\n"
}
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
return
}
app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
if err != nil {
if models.IsErrOauthClientIDInvalid(err) {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeUnauthorizedClient,
ErrorDescription: "Client ID not registered",
State: form.State,
}, "")
return
}
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
if err := app.LoadUser(); err != nil {
ctx.ServerError("LoadUser", err)
return
}
if !app.ContainsRedirectURI(form.RedirectURI) {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeInvalidRequest,
ErrorDescription: "Unregistered Redirect URI",
State: form.State,
}, "")
return
}
if form.ResponseType != "code" {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeUnsupportedResponseType,
ErrorDescription: "Only code response type is supported.",
State: form.State,
}, form.RedirectURI)
return
}
// pkce support
switch form.CodeChallengeMethod {
case "S256":
case "plain":
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "cannot set code challenge method",
State: form.State,
}, form.RedirectURI)
return
}
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "cannot set code challenge",
State: form.State,
}, form.RedirectURI)
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
case "":
break
default:
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeInvalidRequest,
ErrorDescription: "unsupported code challenge method",
State: form.State,
}, form.RedirectURI)
return
}
grant, err := app.GetGrantByUserID(ctx.User.ID)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
// Redirect if user already granted access
if grant != nil {
code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
redirect, err := code.GenerateRedirectURI(form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
// Update nonce to reflect the new session
if len(form.Nonce) > 0 {
err := grant.SetNonce(form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
}
ctx.Redirect(redirect.String(), 302)
return
}
// show authorize page to grant access
ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI
ctx.Data["State"] = form.State
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.User.Name) + "</a>"
ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
// TODO document SESSION <=> FORM
err = ctx.Session.Set("client_id", app.ClientID)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
err = ctx.Session.Set("redirect_uri", form.RedirectURI)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
err = ctx.Session.Set("state", form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
ctx.HTML(http.StatusOK, tplGrantAccess)
}
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
func GrantApplicationOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.GrantApplicationForm)
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
ctx.Session.Get("redirect_uri") != form.RedirectURI {
ctx.Error(http.StatusBadRequest)
return
}
app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
if err != nil {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "cannot create grant for user",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
if len(form.Nonce) > 0 {
err := grant.SetNonce(form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
}
var codeChallenge, codeChallengeMethod string
codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
redirect, err := code.GenerateRedirectURI(form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
ctx.Redirect(redirect.String(), 302)
}
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
ctx.Resp.Header().Set("Content-Type", "application/json")
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
log.Error("%v", err)
ctx.Error(http.StatusInternalServerError)
}
}
// AccessTokenOAuth manages all access token requests by the client
func AccessTokenOAuth(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
if form.ClientID == "" {
authHeader := ctx.Req.Header.Get("Authorization")
authContent := strings.SplitN(authHeader, " ", 2)
if len(authContent) == 2 && authContent[0] == "Basic" {
payload, err := base64.StdEncoding.DecodeString(authContent[1])
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot parse basic auth header",
})
return
}
pair := strings.SplitN(string(payload), ":", 2)
if len(pair) != 2 {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot parse basic auth header",
})
return
}
form.ClientID = pair[0]
form.ClientSecret = pair[1]
}
}
switch form.GrantType {
case "refresh_token":
handleRefreshToken(ctx, form)
return
case "authorization_code":
handleAuthorizationCode(ctx, form)
return
default:
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnsupportedGrantType,
ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
})
}
}
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
token, err := models.ParseOAuth2Token(form.RefreshToken)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
// get grant before increasing counter
grant, err := models.GetOAuth2GrantByID(token.GrantID)
if err != nil || grant == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "grant does not exist",
})
return
}
// check if token got already used
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "token was already used",
})
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
return
}
accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
}
ctx.JSON(http.StatusOK, accessToken)
}
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidClient,
ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
})
return
}
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
authorizationCode, err := models.GetOAuth2AuthorizationByCode(form.Code)
if err != nil || authorizationCode == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
// check if code verifier authorizes the client, PKCE support
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
// check if granted for this application
if authorizationCode.Grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "invalid grant",
})
return
}
// remove token from database to deny duplicate usage
if err := authorizationCode.Invalidate(); err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot proceed your request",
})
}
resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
}
// send successful response
ctx.JSON(http.StatusOK, resp)
}
func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
ctx.JSON(http.StatusBadRequest, acErr)
}
func handleServerError(ctx *context.Context, state string, redirectURI string) {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "A server error occurred",
State: state,
}, redirectURI)
}
func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
if redirectURI == "" {
log.Warn("Authorization failed: %v", authErr.ErrorDescription)
ctx.Data["Error"] = authErr
ctx.HTML(400, tplGrantError)
return
}
redirect, err := url.Parse(redirectURI)
if err != nil {
ctx.ServerError("url.Parse", err)
return
}
q := redirect.Query()
q.Set("error", string(authErr.ErrorCode))
q.Set("error_description", authErr.ErrorDescription)
q.Set("state", authErr.State)
redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String(), 302)
}
func handleBearerTokenError(ctx *context.Context, beErr BearerTokenError) {
ctx.Resp.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"\", error=\"%s\", error_description=\"%s\"", beErr.ErrorCode, beErr.ErrorDescription))
switch beErr.ErrorCode {
case BearerTokenErrorCodeInvalidRequest:
ctx.JSON(http.StatusBadRequest, beErr)
case BearerTokenErrorCodeInvalidToken:
ctx.JSON(http.StatusUnauthorized, beErr)
case BearerTokenErrorCodeInsufficientScope:
ctx.JSON(http.StatusForbidden, beErr)
default:
log.Error("Invalid BearerTokenErrorCode: %v", beErr.ErrorCode)
ctx.ServerError("Unhandled BearerTokenError", fmt.Errorf("BearerTokenError: error=\"%v\", error_description=\"%v\"", beErr.ErrorCode, beErr.ErrorDescription))
}
}

329
routers/web/user/profile.go Normal file
View File

@@ -0,0 +1,329 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 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 user
import (
"fmt"
"net/http"
"path"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/org"
)
// GetUserByName get user by name
func GetUserByName(ctx *context.Context, name string) *models.User {
user, err := models.GetUserByName(name)
if err != nil {
if models.IsErrUserNotExist(err) {
if redirectUserID, err := models.LookupUserRedirect(name); err == nil {
context.RedirectToUser(ctx, name, redirectUserID)
} else {
ctx.NotFound("GetUserByName", err)
}
} else {
ctx.ServerError("GetUserByName", err)
}
return nil
}
return user
}
// GetUserByParams returns user whose name is presented in URL paramenter.
func GetUserByParams(ctx *context.Context) *models.User {
return GetUserByName(ctx, ctx.Params(":username"))
}
// Profile render user's profile page
func Profile(ctx *context.Context) {
uname := ctx.Params(":username")
// Special handle for FireFox requests favicon.ico.
if uname == "favicon.ico" {
ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
return
}
if strings.HasSuffix(uname, ".png") {
ctx.Error(http.StatusNotFound)
return
}
isShowKeys := false
if strings.HasSuffix(uname, ".keys") {
isShowKeys = true
uname = strings.TrimSuffix(uname, ".keys")
}
isShowGPG := false
if strings.HasSuffix(uname, ".gpg") {
isShowGPG = true
uname = strings.TrimSuffix(uname, ".gpg")
}
ctxUser := GetUserByName(ctx, uname)
if ctx.Written() {
return
}
// Show SSH keys.
if isShowKeys {
ShowSSHKeys(ctx, ctxUser.ID)
return
}
// Show GPG keys.
if isShowGPG {
ShowGPGKeys(ctx, ctxUser.ID)
return
}
if ctxUser.IsOrganization() {
org.Home(ctx)
return
}
// Show OpenID URIs
openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["Title"] = ctxUser.DisplayName()
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Owner"] = ctxUser
ctx.Data["OpenIDs"] = openIDs
if setting.Service.EnableUserHeatmap {
data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User)
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUser", err)
return
}
ctx.Data["HeatmapData"] = data
}
if len(ctxUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: map[string]string{"mode": "document"},
}, ctxUser.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["RenderedDescription"] = content
}
showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
orgs, err := models.GetOrgsByUserID(ctxUser.ID, showPrivate)
if err != nil {
ctx.ServerError("GetOrgsByUserIDDesc", err)
return
}
ctx.Data["Orgs"] = orgs
ctx.Data["HasOrgsVisible"] = models.HasOrgsVisible(orgs, ctx.User)
tab := ctx.Query("tab")
ctx.Data["TabName"] = tab
page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}
topicOnly := ctx.QueryBool("topic")
var (
repos []*models.Repository
count int64
total int
orderBy models.SearchOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "newest":
orderBy = models.SearchOrderByNewest
case "oldest":
orderBy = models.SearchOrderByOldest
case "recentupdate":
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = models.SearchOrderByAlphabetically
case "moststars":
orderBy = models.SearchOrderByStarsReverse
case "feweststars":
orderBy = models.SearchOrderByStars
case "mostforks":
orderBy = models.SearchOrderByForksReverse
case "fewestforks":
orderBy = models.SearchOrderByForks
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = models.SearchOrderByRecentUpdated
}
keyword := strings.Trim(ctx.Query("q"), " ")
ctx.Data["Keyword"] = keyword
switch tab {
case "followers":
items, err := ctxUser.GetFollowers(models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
})
if err != nil {
ctx.ServerError("GetFollowers", err)
return
}
ctx.Data["Cards"] = items
total = ctxUser.NumFollowers
case "following":
items, err := ctxUser.GetFollowing(models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
})
if err != nil {
ctx.ServerError("GetFollowing", err)
return
}
ctx.Data["Cards"] = items
total = ctxUser.NumFollowing
case "activity":
retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
Actor: ctx.User,
IncludePrivate: showPrivate,
OnlyPerformedBy: true,
IncludeDeleted: false,
Date: ctx.Query("date"),
})
if ctx.Written() {
return
}
case "stars":
ctx.Data["PageIsProfileStarList"] = true
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
ListOptions: models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
},
Actor: ctx.User,
Keyword: keyword,
OrderBy: orderBy,
Private: ctx.IsSigned,
StarredByID: ctxUser.ID,
Collaborate: util.OptionalBoolFalse,
TopicOnly: topicOnly,
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
total = int(count)
case "projects":
ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
Page: -1,
IsClosed: util.OptionalBoolFalse,
Type: models.ProjectTypeIndividual,
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
}
case "watching":
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
ListOptions: models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
},
Actor: ctx.User,
Keyword: keyword,
OrderBy: orderBy,
Private: ctx.IsSigned,
WatchedByID: ctxUser.ID,
Collaborate: util.OptionalBoolFalse,
TopicOnly: topicOnly,
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
total = int(count)
default:
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
ListOptions: models.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
},
Actor: ctx.User,
Keyword: keyword,
OwnerID: ctxUser.ID,
OrderBy: orderBy,
Private: ctx.IsSigned,
Collaborate: util.OptionalBoolFalse,
TopicOnly: topicOnly,
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
total = int(count)
}
ctx.Data["Repos"] = repos
ctx.Data["Total"] = total
pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.Data["ShowUserEmail"] = len(ctxUser.Email) > 0 && ctx.IsSigned && (!ctxUser.KeepEmailPrivate || ctxUser.ID == ctx.User.ID)
ctx.HTML(http.StatusOK, tplProfile)
}
// Action response for follow/unfollow user request
func Action(ctx *context.Context) {
u := GetUserByParams(ctx)
if ctx.Written() {
return
}
var err error
switch ctx.Params(":action") {
case "follow":
err = models.FollowUser(ctx.User.ID, u.ID)
case "unfollow":
err = models.UnfollowUser(ctx.User.ID, u.ID)
}
if err != nil {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
return
}
ctx.RedirectToFirst(ctx.Query("redirect_to"), u.HomeLink())
}

View File

@@ -0,0 +1,313 @@
// 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"
"net/http"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
)
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
loadAccountData(ctx)
ctx.HTML(http.StatusOK, tplSettingsAccount)
}
// AccountPost response for change user's password
func AccountPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ChangePasswordForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if ctx.HasError() {
loadAccountData(ctx)
ctx.HTML(http.StatusOK, 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 if !password.IsComplexEnough(form.Password) {
ctx.Flash.Error(password.BuildComplexityError(ctx))
} else if pwned, err := password.IsPwned(ctx, form.Password); pwned || err != nil {
errMsg := ctx.Tr("auth.password_pwned")
if err != nil {
log.Error(err.Error())
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Flash.Error(errMsg)
} else {
var err error
if err = ctx.User.SetPassword(form.Password); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "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 := web.GetForm(ctx).(*forms.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
}
// Send activation Email
if ctx.Query("_method") == "SENDACTIVATION" {
var address string
if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
log.Error("Send activation: activation still pending")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if ctx.Query("id") == "PRIMARY" {
if ctx.User.IsActive {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
address = ctx.User.Email
} else {
id := ctx.QueryInt64("id")
email, err := models.GetEmailAddressByID(ctx.User.ID, id)
if err != nil {
log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email == nil {
log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email.IsActivated {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateEmailMail(ctx.User, email)
address = email.Email
}
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
if !(preference == models.EmailNotificationsEnabled ||
preference == models.EmailNotificationsOnMention ||
preference == models.EmailNotificationsDisabled) {
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.User.Name)
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return
}
if err := ctx.User.SetEmailNotifications(preference); err != nil {
log.Error("Set Email Notifications failed: %v", err)
ctx.ServerError("SetEmailNotifications", err)
return
}
log.Trace("Email notifications preference made %s: %s", preference, ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if ctx.HasError() {
loadAccountData(ctx)
ctx.HTML(http.StatusOK, 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) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
return
} else if models.IsErrEmailInvalid(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
return
}
ctx.ServerError("AddEmailAddress", err)
return
}
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.User, email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.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(http.StatusOK, 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) {
loadAccountData(ctx)
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 + "/")
}
}
// UpdateUIThemePost is used to update users' specific theme
func UpdateUIThemePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateThemeForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if ctx.HasError() {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if !form.IsThemeExists() {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if err := ctx.User.UpdateTheme(form.Theme); err != nil {
ctx.Flash.Error(ctx.Tr("settings.theme_update_error"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
log.Trace("Update user theme: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.theme_update_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
func loadAccountData(ctx *context.Context) {
emlist, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
type UserEmail struct {
models.EmailAddress
CanBePrimary bool
}
pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
emails := make([]*UserEmail, len(emlist))
for i, em := range emlist {
var email UserEmail
email.EmailAddress = *em
email.CanBePrimary = em.IsActivated
emails[i] = &email
}
ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())
}
}

View File

@@ -0,0 +1,99 @@
// 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 (
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
)
func TestChangePassword(t *testing.T) {
oldPassword := "password"
setting.MinPasswordLength = 6
var pcALL = []string{"lower", "upper", "digit", "spec"}
var pcLUN = []string{"lower", "upper", "digit"}
var pcLU = []string{"lower", "upper"}
for _, req := range []struct {
OldPassword string
NewPassword string
Retype string
Message string
PasswordComplexity []string
}{
{
OldPassword: oldPassword,
NewPassword: "Qwerty123456-",
Retype: "Qwerty123456-",
Message: "",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "12345",
Retype: "12345",
Message: "auth.password_too_short",
PasswordComplexity: pcALL,
},
{
OldPassword: "12334",
NewPassword: "123456",
Retype: "123456",
Message: "settings.password_incorrect",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "123456",
Retype: "12345",
Message: "form.password_not_match",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "Qwerty",
Retype: "Qwerty",
Message: "form.password_complexity",
PasswordComplexity: pcALL,
},
{
OldPassword: oldPassword,
NewPassword: "Qwerty",
Retype: "Qwerty",
Message: "form.password_complexity",
PasswordComplexity: pcLUN,
},
{
OldPassword: oldPassword,
NewPassword: "QWERTY",
Retype: "QWERTY",
Message: "form.password_complexity",
PasswordComplexity: pcLU,
},
} {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user/settings/security")
test.LoadUser(t, ctx, 2)
test.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.ChangePasswordForm{
OldPassword: req.OldPassword,
Password: req.NewPassword,
Retype: req.Retype,
})
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2020 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"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// AdoptOrDeleteRepository adopts or deletes a repository
func AdoptOrDeleteRepository(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsRepos"] = true
allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowAdopt"] = allowAdopt
allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
ctx.Data["allowDelete"] = allowDelete
dir := ctx.Query("id")
action := ctx.Query("action")
ctxUser := ctx.User
root := filepath.Join(models.UserPath(ctxUser.LowerName))
// check not a repo
has, err := models.IsRepositoryExist(ctxUser, dir)
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
}
isDir, err := util.IsDir(filepath.Join(root, dir+".git"))
if err != nil {
ctx.ServerError("IsDir", err)
return
}
if has || !isDir {
// Fallthrough to failure mode
} else if action == "adopt" && allowAdopt {
if _, err := repository.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{
Name: dir,
IsPrivate: true,
}); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.adopt_preexisting_success", dir))
} else if action == "delete" && allowDelete {
if err := repository.DeleteUnadoptedRepository(ctxUser, ctxUser, dir); err != nil {
ctx.ServerError("repository.AdoptRepository", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/repos")
}

View File

@@ -0,0 +1,106 @@
// 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 (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
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
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
}
// ApplicationsPost response for add user's access token
func ApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewAccessTokenForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
t := &models.AccessToken{
UID: ctx.User.ID,
Name: form.Name,
}
exist, err := models.AccessTokenByNameExists(t)
if err != nil {
ctx.ServerError("AccessTokenByNameExists", err)
return
}
if exist {
ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name))
ctx.Redirect(setting.AppSubURL + "/user/settings/applications")
return
}
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.Token)
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(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/applications",
})
}
func loadApplicationsData(ctx *context.Context) {
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.User.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
if setting.OAuth2.Enable {
ctx.Data["Applications"], err = models.GetOAuth2ApplicationsByUserID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
ctx.Data["Grants"], err = models.GetOAuth2GrantsByUserID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetOAuth2GrantsByUserID", err)
return
}
}
}

View File

@@ -0,0 +1,226 @@
// 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 (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
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
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
loadKeysData(ctx)
ctx.HTML(http.StatusOK, tplSettingsKeys)
}
// KeysPost response for change user's SSH/GPG keys
func KeysPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddKeyForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled
if ctx.HasError() {
loadKeysData(ctx)
ctx.HTML(http.StatusOK, tplSettingsKeys)
return
}
switch form.Type {
case "principal":
content, err := models.CheckPrincipalKeyString(ctx.User, form.Content)
if err != nil {
if models.IsErrSSHDisabled(err) {
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
} else {
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
}
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if _, err = models.AddPrincipalKey(ctx.User.ID, content, 0); err != nil {
ctx.Data["HasPrincipalError"] = true
switch {
case models.IsErrKeyAlreadyExist(err), models.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPrincipalKey", err)
}
return
}
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
keys, 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):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
case models.IsErrGPGNoEmailFound(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
}
return
}
keyIDs := ""
for _, key := range keys {
keyIDs += key.KeyID
keyIDs += ", "
}
if len(keyIDs) > 0 {
keyIDs = keyIDs[:len(keyIDs)-2]
}
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
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, 0); err != nil {
ctx.Data["HasSSHError"] = true
switch {
case models.IsErrKeyAlreadyExist(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
case models.IsErrKeyNameAlreadyUsed(err):
loadKeysData(ctx)
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
case models.IsErrKeyUnableVerify(err):
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
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":
keyID := ctx.QueryInt64("id")
external, err := models.PublicKeyIsExternallyManaged(keyID)
if err != nil {
ctx.ServerError("sshKeysExternalManaged", err)
return
}
if external {
ctx.Flash.Error(ctx.Tr("setting.ssh_externally_managed"))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
if err := models.DeletePublicKey(ctx.User, keyID); err != nil {
ctx.Flash.Error("DeletePublicKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
}
case "principal":
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_principal_deletion_success"))
}
default:
ctx.Flash.Warning("Function not implemented")
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/keys",
})
}
func loadKeysData(ctx *context.Context) {
keys, err := models.ListPublicKeys(ctx.User.ID, models.ListOptions{})
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["Keys"] = keys
externalKeys, err := models.PublicKeysAreExternallyManaged(keys)
if err != nil {
ctx.ServerError("ListPublicKeys", err)
return
}
ctx.Data["ExternalKeys"] = externalKeys
gpgkeys, err := models.ListGPGKeys(ctx.User.ID, models.ListOptions{})
if err != nil {
ctx.ServerError("ListGPGKeys", err)
return
}
ctx.Data["GPGKeys"] = gpgkeys
principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
if err != nil {
ctx.ServerError("ListPrincipalKeys", err)
return
}
ctx.Data["Principals"] = principals
}

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,159 @@
// Copyright 2019 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 (
"fmt"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
const (
tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
)
// OAuthApplicationsPost response for adding a oauth2 application
func OAuthApplicationsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
// TODO validate redirect URI
app, err := models.CreateOAuth2Application(models.CreateOAuth2ApplicationOptions{
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
UserID: ctx.User.ID,
})
if err != nil {
ctx.ServerError("CreateOAuth2Application", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
}
// OAuthApplicationsEdit response for editing oauth2 application
func OAuthApplicationsEdit(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
// TODO validate redirect URI
var err error
if ctx.Data["App"], err = models.UpdateOAuth2Application(models.UpdateOAuth2ApplicationOptions{
ID: ctx.ParamsInt64("id"),
Name: form.Name,
RedirectURIs: []string{form.RedirectURI},
UserID: ctx.User.ID,
}); err != nil {
ctx.ServerError("UpdateOAuth2Application", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
}
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
if err != nil {
if models.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound("Application not found", err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != ctx.User.ID {
ctx.NotFound("Application not found", nil)
return
}
ctx.Data["App"] = app
ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
if err != nil {
ctx.ServerError("GenerateClientSecret", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
}
// OAuth2ApplicationShow displays the given application
func OAuth2ApplicationShow(ctx *context.Context) {
app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
if err != nil {
if models.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound("Application not found", err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != ctx.User.ID {
ctx.NotFound("Application not found", nil)
return
}
ctx.Data["App"] = app
ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
}
// DeleteOAuth2Application deletes the given oauth2 application
func DeleteOAuth2Application(ctx *context.Context) {
if err := models.DeleteOAuth2Application(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
ctx.ServerError("DeleteOAuth2Application", err)
return
}
log.Trace("OAuth2 Application deleted: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/applications",
})
}
// RevokeOAuth2Grant revokes the grant with the given id
func RevokeOAuth2Grant(ctx *context.Context) {
if ctx.User.ID == 0 || ctx.QueryInt64("id") == 0 {
ctx.ServerError("RevokeOAuth2Grant", fmt.Errorf("user id or grant id is zero"))
return
}
if err := models.RevokeOAuth2Grant(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
ctx.ServerError("RevokeOAuth2Grant", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/applications",
})
}

View File

@@ -0,0 +1,319 @@
// 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"
"net/http"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/forms"
"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(http.StatusOK, tplSettingsProfile)
}
// HandleUsernameChange handle username changes from user settings and admin interface
func HandleUsernameChange(ctx *context.Context, user *models.User, newName string) error {
// Non-local users are not allowed to change their username.
if !user.IsLocal() {
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
}
// Check if user name has been changed
if user.LowerName != strings.ToLower(newName) {
if err := models.ChangeUserName(user, newName); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
case models.IsErrEmailAlreadyUsed(err):
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
case models.IsErrNameReserved(err):
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
case models.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
case models.IsErrNameCharsNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
default:
ctx.ServerError("ChangeUserName", err)
}
return err
}
} else {
if err := models.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
ctx.ServerError("UpdateRepository", err)
return err
}
}
log.Trace("User name changed: %s -> %s", user.Name, newName)
return nil
}
// ProfilePost response for change user's profile
func ProfilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsProfile"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile)
return
}
if len(form.Name) != 0 && ctx.User.Name != form.Name {
log.Debug("Changing name for %s to %s", ctx.User.Name, form.Name)
if err := HandleUsernameChange(ctx, ctx.User, form.Name); err != nil {
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.User.Name = form.Name
ctx.User.LowerName = strings.ToLower(form.Name)
}
ctx.User.FullName = form.FullName
ctx.User.KeepEmailPrivate = form.KeepEmailPrivate
ctx.User.Website = form.Website
ctx.User.Location = form.Location
if len(form.Language) != 0 {
if !util.IsStringInSlice(form.Language, setting.Langs) {
ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
ctx.User.Language = form.Language
}
ctx.User.Description = form.Description
ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
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
middleware.SetLocaleCookie(ctx.Resp, ctx.User.Language, 0)
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 *forms.AvatarForm, ctxUser *models.User) error {
ctxUser.UseCustomAvatar = form.Source == forms.AvatarLocal
if len(form.Gravatar) > 0 {
if form.Avatar != nil {
ctxUser.Avatar = base.EncodeMD5(form.Gravatar)
} else {
ctxUser.Avatar = ""
}
ctxUser.AvatarEmail = form.Gravatar
}
if form.Avatar != nil && form.Avatar.Filename != "" {
fr, err := form.Avatar.Open()
if err != nil {
return fmt.Errorf("Avatar.Open: %v", err)
}
defer fr.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize {
return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
}
data, err := ioutil.ReadAll(fr)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
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 if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" {
// No avatar is uploaded but setting has been changed to enable,
// generate a random one when needed.
if err := ctxUser.GenerateRandomAvatar(); err != nil {
log.Error("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 := web.GetForm(ctx).(*forms.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(http.StatusOK, 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
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
opts := models.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.QueryInt("page"),
}
if opts.Page <= 0 {
opts.Page = 1
}
start := (opts.Page - 1) * opts.PageSize
end := start + opts.PageSize
adoptOrDelete := ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories)
ctxUser := ctx.User
count := 0
if adoptOrDelete {
repoNames := make([]string, 0, setting.UI.Admin.UserPagingNum)
repos := map[string]*models.Repository{}
// We're going to iterate by pagesize.
root := filepath.Join(models.UserPath(ctxUser.Name))
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !info.IsDir() || path == root {
return nil
}
name := info.Name()
if !strings.HasSuffix(name, ".git") {
return filepath.SkipDir
}
name = name[:len(name)-4]
if models.IsUsableRepoName(name) != nil || strings.ToLower(name) != name {
return filepath.SkipDir
}
if count >= start && count < end {
repoNames = append(repoNames, name)
}
count++
return filepath.SkipDir
}); err != nil {
ctx.ServerError("filepath.Walk", err)
return
}
if err := ctxUser.GetRepositories(models.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
for _, repo := range ctxUser.Repos {
if repo.IsFork {
if err := repo.GetBaseRepo(); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
}
repos[repo.LowerName] = repo
}
ctx.Data["Dirs"] = repoNames
ctx.Data["ReposMap"] = repos
} else {
var err error
var count64 int64
ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
if err != nil {
ctx.ServerError("GetRepositories", err)
return
}
count = int(count64)
repos := ctxUser.Repos
for i := range repos {
if repos[i].IsFork {
if err := repos[i].GetBaseRepo(); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
}
}
ctx.Data["Repos"] = repos
}
ctx.Data["Owner"] = ctxUser
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsRepositories)
}

View File

@@ -0,0 +1,111 @@
// 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 (
"net/http"
"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
ctx.Data["RequireU2F"] = true
if ctx.Query("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
}
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
id := ctx.QueryInt64("id")
if id <= 0 {
ctx.Flash.Error("Account link id is not given")
} else {
if _, err := models.RemoveAccountLink(ctx.User, id); err != nil {
ctx.Flash.Error("RemoveAccountLink: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success"))
}
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}
func loadSecurityData(ctx *context.Context) {
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
if enrolled {
ctx.Data["U2FRegistrations"], err = models.GetU2FRegistrationsByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetU2FRegistrationsByUID", err)
return
}
}
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: 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
openid, err := models.GetUserOpenIDs(ctx.User.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
}

View File

@@ -0,0 +1,129 @@
// 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 (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
// OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddOpenIDForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.HasError() {
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
return
}
// WARNING: specifying a wrong OpenID here could lock
// a user out of her account, would be better to
// verify/confirm the new OpenID before storing it
// Also, consider allowing for multiple OpenID URIs
id, err := openid.Normalize(form.Openid)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
return
}
form.Openid = id
log.Trace("Normalized id: " + id)
oids, err := models.GetUserOpenIDs(ctx.User.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = oids
// Check that the OpenID is not already used
for _, obj := range oids {
if obj.URI == id {
loadSecurityData(ctx)
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
return
}
}
redirectTo := setting.AppURL + "user/settings/security"
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form)
return
}
ctx.Redirect(url)
}
func settingsOpenIDVerify(ctx *context.Context) {
log.Trace("Incoming call to: " + ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: " + fullURL)
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: " + id)
oid := &models.UserOpenID{UID: ctx.User.ID, URI: id}
if err = models.AddUserOpenID(oid); err != nil {
if models.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
log.Trace("Associated OpenID %s to user %s", id, ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if err := models.DeleteUserOpenID(&models.UserOpenID{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil {
ctx.ServerError("DeleteUserOpenID", err)
return
}
log.Trace("OpenID address deleted: %s", ctx.User.Name)
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}
// ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) {
if err := models.ToggleUserOpenIDVisibility(ctx.QueryInt64("id")); err != nil {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@@ -0,0 +1,250 @@
// 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"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"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 {
if models.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
return
}
if err = models.UpdateTwoFactor(t); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
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 {
if models.IsErrTwoFactorNotEnrolled(err) {
ctx.Flash.Error(ctx.Tr("setting.twofa_not_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
return
}
if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
// There is a potential DB race here - we must have been disabled by another request in the intervening period
ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", 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 err != nil {
ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
return false
}
}
// Filter unsafe character ':' in issuer
issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
if otpKey == nil {
otpKey, err = totp.Generate(totp.GenerateOpts{
SecretSize: 40,
Issuer: issuer,
AccountName: ctx.User.Name,
})
if err != nil {
ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
return false
}
}
ctx.Data["TwofaSecret"] = otpKey.Secret()
img, err := otpKey.Image(320, 240)
if err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
return false
}
var imgBytes bytes.Buffer
if err = png.Encode(&imgBytes, img); err != nil {
ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
return false
}
ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
return false
}
if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
return false
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
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 - we should redirect back!
log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.User)
ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
return
}
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
}
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
t, err := models.GetTwoFactorByUID(ctx.User.ID)
if t != nil {
// already enrolled
ctx.Flash.Error(ctx.Tr("setting.twofa_is_enrolled"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
}
if err != nil && !models.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
return
}
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
return
}
secretRaw := ctx.Session.Get("twofaSecret")
if secretRaw == nil {
ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
secret := secretRaw.(string)
if !totp.Validate(form.Passcode, secret) {
if !twofaGenerateSecretAndQr(ctx) {
return
}
ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
return
}
t = &models.TwoFactor{
UID: ctx.User.ID,
}
err = t.SetSecret(secret)
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
return
}
token, err := t.GenerateScratchToken()
if err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
return
}
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
}
if err := ctx.Session.Delete("twofaUri"); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to delete twofaUri from the session: Error: %v", err)
}
if err := ctx.Session.Release(); err != nil {
// tolerate this failure - it's more important to continue
log.Error("Unable to save changes to the session: %v", err)
}
if err = models.NewTwoFactor(t); err != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@@ -0,0 +1,111 @@
// 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"
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
"github.com/tstranex/u2f"
)
// U2FRegister initializes the u2f registration procedure
func U2FRegister(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.U2FRegistrationForm)
if form.Name == "" {
ctx.Error(http.StatusConflict)
return
}
challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
if err != nil {
ctx.ServerError("NewChallenge", err)
return
}
if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
ctx.ServerError("Unable to set session key for u2fChallenge", err)
return
}
regs, err := models.GetU2FRegistrationsByUID(ctx.User.ID)
if err != nil {
ctx.ServerError("GetU2FRegistrationsByUID", err)
return
}
for _, reg := range regs {
if reg.Name == form.Name {
ctx.Error(http.StatusConflict, "Name already taken")
return
}
}
if err := ctx.Session.Set("u2fName", form.Name); err != nil {
ctx.ServerError("Unable to set session key for u2fName", err)
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
ctx.JSON(http.StatusOK, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations()))
}
// U2FRegisterPost receives the response of the security key
func U2FRegisterPost(ctx *context.Context) {
response := web.GetForm(ctx).(*u2f.RegisterResponse)
challSess := ctx.Session.Get("u2fChallenge")
u2fName := ctx.Session.Get("u2fName")
if challSess == nil || u2fName == nil {
ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session"))
return
}
challenge := challSess.(*u2f.Challenge)
name := u2fName.(string)
config := &u2f.Config{
// Chrome 66+ doesn't return the device's attestation
// certificate by default.
SkipAttestationVerify: true,
}
reg, err := u2f.Register(*response, *challenge, config)
if err != nil {
ctx.ServerError("u2f.Register", err)
return
}
if _, err = models.CreateRegistration(ctx.User, name, reg); err != nil {
ctx.ServerError("u2f.Register", err)
return
}
ctx.Status(200)
}
// U2FDelete deletes an security key by id
func U2FDelete(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.U2FDeleteForm)
reg, err := models.GetU2FRegistrationByID(form.ID)
if err != nil {
if models.IsErrU2FRegistrationNotExist(err) {
ctx.Status(200)
return
}
ctx.ServerError("GetU2FRegistrationByID", err)
return
}
if reg.UserID != ctx.User.ID {
ctx.Status(401)
return
}
if err := models.DeleteRegistration(reg); err != nil {
ctx.ServerError("DeleteRegistration", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": setting.AppSubURL + "/user/settings/security",
})
}

32
routers/web/user/task.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright 2020 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 user
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
)
// TaskStatus returns task's status
func TaskStatus(ctx *context.Context) {
task, opts, err := models.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.User.ID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"err": err,
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": task.Status,
"err": task.Errors,
"repo-id": task.RepoID,
"repo-name": opts.RepoName,
"start": task.StartTime,
"end": task.EndTime,
})
}

1023
routers/web/web.go Normal file

File diff suppressed because it is too large Load Diff