- path: modules/log/
linters:
- errcheck
- - path: routers/routes/web.go
- linters:
- - dupl
- path: routers/api/v1/repo/issue_subscription.go
linters:
- dupl
linters:
- staticcheck
text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
+
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers"
- "code.gitea.io/gitea/routers/routes"
+ "code.gitea.io/gitea/routers/install"
context2 "github.com/gorilla/context"
"github.com/urfave/cli"
}
// Perform pre-initialization
- needsInstall := routers.PreInstallInit(graceful.GetManager().HammerContext())
+ needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext())
if needsInstall {
// Flag for port number in case first time run conflict
if ctx.IsSet("port") {
return err
}
}
- c := routes.InstallRoutes()
+ c := install.Routes()
err := listen(c, false)
select {
case <-graceful.GetManager().IsShutdown():
}
// Set up Chi routes
- c := routes.NormalRoutes()
+ c := routers.NormalRoutes()
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers"
- "code.gitea.io/gitea/routers/routes"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
//routers.GlobalInit()
external.RegisterRenderers()
markup.Init()
- c := routes.NormalRoutes()
+ c := routers.NormalRoutes()
log.Printf("[PR] Ready for testing !\n")
log.Printf("[PR] Login with user1, user2, user3, ... with pass: password\n")
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/routes"
+ "code.gitea.io/gitea/routers"
"gitea.com/go-chi/session"
jsoniter "github.com/json-iterator/go"
oldSessionConfig := setting.SessionConfig.ProviderConfig
defer func() {
setting.SessionConfig.ProviderConfig = oldSessionConfig
- c = routes.NormalRoutes()
+ c = routers.NormalRoutes()
}()
var config session.Options
setting.SessionConfig.ProviderConfig = string(newConfigBytes)
- c = routes.NormalRoutes()
+ c = routers.NormalRoutes()
t.Run("NoSessionOnViewIssue", func(t *testing.T) {
defer PrintCurrentTest(t)()
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
- "code.gitea.io/gitea/routers/routes"
"github.com/PuerkitoBio/goquery"
jsoniter "github.com/json-iterator/go"
defer cancel()
initIntegrationTest()
- c = routes.NormalRoutes()
+ c = routers.NormalRoutes()
// integration test settings...
if setting.Cfg != nil {
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/routers/routes"
+ "code.gitea.io/gitea/routers/web"
jsoniter "github.com/json-iterator/go"
gzipp "github.com/klauspost/compress/gzip"
t.Skip()
return
}
- content := make([]byte, routes.GzipMinSize*10)
+ content := make([]byte, web.GzipMinSize*10)
for i := range content {
content[i] = byte(i % 256)
}
t.Skip()
return
}
- b := make([]byte, routes.GzipMinSize*10)
+ b := make([]byte, web.GzipMinSize*10)
for i := range b {
b[i] = byte(i % 256)
}
t.Skip()
return
}
- b := make([]byte, routes.GzipMinSize*10)
+ b := make([]byte, web.GzipMinSize*10)
for i := range b {
b[i] = byte(i % 256)
}
+++ /dev/null
-// 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))
-}
+++ /dev/null
-// 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))
- }
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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())
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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("..", ".."))
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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"
-)
-
-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
-
- routers.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)
-}
+++ /dev/null
-// 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"
- 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
-
- routers.RenderRepoSearch(ctx, &routers.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))
-}
+++ /dev/null
-// 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"
- router_user_setting "code.gitea.io/gitea/routers/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
-
- routers.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",
- })
-}
+++ /dev/null
-// 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)
-}
"code.gitea.io/gitea/modules/repofiles"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/repo"
+ "code.gitea.io/gitea/routers/common"
)
// GetRawFile get a file by path on a repository
}
return
}
- if err = repo.ServeBlob(ctx.Context, blob); err != nil {
+ if err = common.ServeBlob(ctx.Context, blob); err != nil {
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
}
}
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
- repo.Download(ctx.Context)
+ common.Download(ctx.Context)
}
// GetEditorconfig get editor config of a repository
--- /dev/null
+// 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 common
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/migrations"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// InitDBEngine In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
+func InitDBEngine(ctx context.Context) (err error) {
+ log.Info("Beginning ORM engine initialization.")
+ for i := 0; i < setting.Database.DBConnectRetries; i++ {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
+ default:
+ }
+ log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
+ if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
+ break
+ } else if i == setting.Database.DBConnectRetries-1 {
+ return err
+ }
+ log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
+ log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
+ time.Sleep(setting.Database.DBConnectBackoff)
+ }
+ models.HasEngine = true
+ return nil
+}
--- /dev/null
+// 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 common
+
+import (
+ "net/http"
+ "time"
+
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+)
+
+// LoggerHandler is a handler that will log the routing to the default gitea log
+func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ start := time.Now()
+
+ _ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
+
+ next.ServeHTTP(w, req)
+
+ var status int
+ if v, ok := w.(context.ResponseWriter); ok {
+ status = v.Status()
+ }
+
+ _ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
+ })
+ }
+}
--- /dev/null
+// 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 common
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/chi-middleware/proxy"
+ "github.com/go-chi/chi/middleware"
+)
+
+// Middlewares returns common middlewares
+func Middlewares() []func(http.Handler) http.Handler {
+ var handlers = []func(http.Handler) http.Handler{
+ func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ next.ServeHTTP(context.NewResponse(resp), req)
+ })
+ },
+ }
+
+ if setting.ReverseProxyLimit > 0 {
+ opt := proxy.NewForwardedHeadersOptions().
+ WithForwardLimit(setting.ReverseProxyLimit).
+ ClearTrustedProxies()
+ for _, n := range setting.ReverseProxyTrustedProxies {
+ if !strings.Contains(n, "/") {
+ opt.AddTrustedProxy(n)
+ } else {
+ opt.AddTrustedNetwork(n)
+ }
+ }
+ handlers = append(handlers, proxy.ForwardedHeaders(opt))
+ }
+
+ handlers = append(handlers, middleware.StripSlashes)
+
+ if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
+ if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
+ handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
+ }
+ }
+ if setting.EnableAccessLog {
+ handlers = append(handlers, context.AccessLogger())
+ }
+
+ handlers = append(handlers, func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ // Why we need this? The Recovery() will try to render a beautiful
+ // error page for user, but the process can still panic again, and other
+ // middleware like session also may panic then we have to recover twice
+ // and send a simple error page that should not panic any more.
+ defer func() {
+ if err := recover(); err != nil {
+ combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+ log.Error("%v", combinedErr)
+ if setting.IsProd() {
+ http.Error(resp, http.StatusText(500), 500)
+ } else {
+ http.Error(resp, combinedErr, 500)
+ }
+ }
+ }()
+ next.ServeHTTP(resp, req)
+ })
+ })
+ return handlers
+}
--- /dev/null
+// 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 common
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/httpcache"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
+ "code.gitea.io/gitea/services/archiver"
+)
+
+// ServeBlob download a git.Blob
+func ServeBlob(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
+ }
+ defer func() {
+ if err = dataRc.Close(); err != nil {
+ log.Error("ServeBlob: Close: %v", err)
+ }
+ }()
+
+ return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
+}
+
+// Download an archive of a repository
+func Download(ctx *context.Context) {
+ uri := ctx.Params("*")
+ aReq := archiver.DeriveRequestFrom(ctx, uri)
+
+ if aReq == nil {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
+ complete := aReq.IsComplete()
+ if !complete {
+ aReq = archiver.ArchiveRepository(aReq)
+ complete = aReq.WaitForCompletion(ctx)
+ }
+
+ if complete {
+ ctx.ServeFile(aReq.GetArchivePath(), downloadName)
+ } else {
+ ctx.Error(http.StatusNotFound)
+ }
+}
+
+// ServeData download file from io.Reader
+func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
+ buf := make([]byte, 1024)
+ n, err := reader.Read(buf)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if n >= 0 {
+ buf = buf[:n]
+ }
+
+ ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
+
+ if size >= 0 {
+ ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
+ } else {
+ log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
+ }
+ name = path.Base(name)
+
+ // Google Chrome dislike commas in filenames, so let's change it to a space
+ name = strings.ReplaceAll(name, ",", " ")
+
+ st := typesniffer.DetectContentType(buf)
+
+ if st.IsText() || ctx.QueryBool("render") {
+ cs, err := charset.DetectEncoding(buf)
+ if err != nil {
+ log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
+ cs = "utf-8"
+ }
+ ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
+ } else {
+ ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
+
+ if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
+ ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
+ if st.IsSvgImage() {
+ ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+ ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
+ ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
+ }
+ } else {
+ ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
+ if setting.MimeTypeMap.Enabled {
+ fileExtension := strings.ToLower(filepath.Ext(name))
+ if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
+ ctx.Resp.Header().Set("Content-Type", mimetype)
+ }
+ }
+ }
+ }
+
+ _, err = ctx.Resp.Write(buf)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(ctx.Resp, reader)
+ return err
+}
+++ /dev/null
-// 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("*")))
-}
+++ /dev/null
-// 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/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()
-}
+++ /dev/null
-// 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 routers
-
-import (
- "bytes"
- "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/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/user"
-)
-
-const (
- // tplHome home page template
- tplHome base.TplName = "home"
- // tplExploreRepos explore repositories page template
- tplExploreRepos base.TplName = "explore/repos"
- // tplExploreUsers explore users page template
- tplExploreUsers base.TplName = "explore/users"
- // tplExploreOrganizations explore organizations page template
- tplExploreOrganizations base.TplName = "explore/organizations"
- // tplExploreCode explore code page template
- tplExploreCode base.TplName = "explore/code"
-)
-
-// 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)
-}
-
-// RepoSearchOptions when calling search repositories
-type RepoSearchOptions struct {
- OwnerID int64
- Private bool
- Restricted bool
- PageSize int
- TplName base.TplName
-}
-
-var (
- nullByte = []byte{0x00}
-)
-
-func isKeywordValid(keyword string) bool {
- return !bytes.Contains([]byte(keyword), nullByte)
-}
-
-// 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)
-}
-
-// ExploreRepos render explore repositories page
-func ExploreRepos(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,
- })
-}
-
-// 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)
-}
-
-// ExploreUsers render explore users page
-func ExploreUsers(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)
-}
-
-// ExploreOrganizations render explore organizations page
-func ExploreOrganizations(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)
-}
-
-// ExploreCode render explore code page
-func ExploreCode(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)
-}
-
-// NotFound render 404 page
-func NotFound(ctx *context.Context) {
- ctx.Data["Title"] = "Page Not Found"
- ctx.NotFound("home.NotFound", nil)
-}
import (
"context"
- "fmt"
"strings"
- "time"
"code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/cron"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/task"
"code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/modules/web"
+ apiv1 "code.gitea.io/gitea/routers/api/v1"
+ "code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/routers/private"
+ web_routers "code.gitea.io/gitea/routers/web"
"code.gitea.io/gitea/services/mailer"
mirror_service "code.gitea.io/gitea/services/mirror"
pull_service "code.gitea.io/gitea/services/pull"
notification.NewContext()
}
-// In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
-func initDBEngine(ctx context.Context) (err error) {
- log.Info("Beginning ORM engine initialization.")
- for i := 0; i < setting.Database.DBConnectRetries; i++ {
- select {
- case <-ctx.Done():
- return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
- default:
- }
- log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
- if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
- break
- } else if i == setting.Database.DBConnectRetries-1 {
- return err
- }
- log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
- log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
- time.Sleep(setting.Database.DBConnectBackoff)
- }
- models.HasEngine = true
- return nil
-}
-
-// PreInstallInit preloads the configuration to check if we need to run install
-func PreInstallInit(ctx context.Context) bool {
- setting.NewContext()
- if !setting.InstallLock {
- log.Trace("AppPath: %s", setting.AppPath)
- log.Trace("AppWorkPath: %s", setting.AppWorkPath)
- log.Trace("Custom path: %s", setting.CustomPath)
- log.Trace("Log path: %s", setting.LogRootPath)
- log.Trace("Preparing to run install page")
- translation.InitLocales()
- if setting.EnableSQLite3 {
- log.Info("SQLite3 Supported")
- }
- setting.InitDBConfig()
- svg.Init()
- }
-
- return !setting.InstallLock
-}
-
-// PostInstallInit rereads the settings and starts up the database
-func PostInstallInit(ctx context.Context) {
- setting.NewContext()
- setting.InitDBConfig()
- if setting.InstallLock {
- if err := initDBEngine(ctx); err == nil {
- log.Info("ORM engine initialization successful!")
- } else {
- log.Fatal("ORM engine initialization failed: %v", err)
- }
- svg.Init()
- }
-}
-
// GlobalInit is for global configuration reload-able.
func GlobalInit(ctx context.Context) {
setting.NewContext()
} else if setting.Database.UseSQLite3 {
log.Fatal("SQLite3 is set in settings but NOT Supported")
}
- if err := initDBEngine(ctx); err == nil {
+ if err := common.InitDBEngine(ctx); err == nil {
log.Info("ORM engine initialization successful!")
} else {
log.Fatal("ORM engine initialization failed: %v", err)
svg.Init()
}
+
+// NormalRoutes represents non install routes
+func NormalRoutes() *web.Route {
+ r := web.NewRoute()
+ for _, middle := range common.Middlewares() {
+ r.Use(middle)
+ }
+
+ r.Mount("/", web_routers.Routes())
+ r.Mount("/api/v1", apiv1.Routes())
+ r.Mount("/api/internal", private.Routes())
+ return r
+}
+++ /dev/null
-// 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 routers
-
-import (
- "fmt"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "time"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/generate"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/user"
- "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"
-
- "gitea.com/go-chi/session"
- "gopkg.in/ini.v1"
-)
-
-const (
- // tplInstall template for installation page
- tplInstall base.TplName = "install"
- tplPostInstall base.TplName = "post-install"
-)
-
-// InstallInit prepare for rendering installation page
-func InstallInit(next http.Handler) http.Handler {
- var rnd = templates.HTMLRenderer()
-
- return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- if setting.InstallLock {
- resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
- _ = rnd.HTML(resp, 200, string(tplPostInstall), nil)
- return
- }
- var locale = middleware.Locale(resp, req)
- var startTime = time.Now()
- var ctx = context.Context{
- Resp: context.NewResponse(resp),
- Flash: &middleware.Flash{},
- Locale: locale,
- Render: rnd,
- Session: session.GetSession(req),
- Data: map[string]interface{}{
- "Title": locale.Tr("install.install"),
- "PageIsInstall": true,
- "DbOptions": setting.SupportedDatabases,
- "i18n": locale,
- "Language": locale.Language(),
- "Lang": locale.Language(),
- "AllLangs": translation.AllLangs(),
- "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
- "PageStartTime": startTime,
- "TmplLoadTimes": func() string {
- return time.Since(startTime).String()
- },
- "PasswordHashAlgorithms": models.AvailableHashAlgorithms,
- },
- }
- for _, lang := range translation.AllLangs() {
- if lang.Lang == locale.Language() {
- ctx.Data["LangName"] = lang.Name
- break
- }
- }
- ctx.Req = context.WithContext(req, &ctx)
- next.ServeHTTP(resp, ctx.Req)
- })
-}
-
-// Install render installation page
-func Install(ctx *context.Context) {
- form := forms.InstallForm{}
-
- // Database settings
- form.DbHost = setting.Database.Host
- form.DbUser = setting.Database.User
- form.DbPasswd = setting.Database.Passwd
- form.DbName = setting.Database.Name
- form.DbPath = setting.Database.Path
- form.DbSchema = setting.Database.Schema
- form.Charset = setting.Database.Charset
-
- var curDBOption = "MySQL"
- switch setting.Database.Type {
- case "postgres":
- curDBOption = "PostgreSQL"
- case "mssql":
- curDBOption = "MSSQL"
- case "sqlite3":
- if setting.EnableSQLite3 {
- curDBOption = "SQLite3"
- }
- }
-
- ctx.Data["CurDbOption"] = curDBOption
-
- // Application general settings
- form.AppName = setting.AppName
- form.RepoRootPath = setting.RepoRootPath
- form.LFSRootPath = setting.LFS.Path
-
- // Note(unknown): it's hard for Windows users change a running user,
- // so just use current one if config says default.
- if setting.IsWindows && setting.RunUser == "git" {
- form.RunUser = user.CurrentUsername()
- } else {
- form.RunUser = setting.RunUser
- }
-
- form.Domain = setting.Domain
- form.SSHPort = setting.SSH.Port
- form.HTTPPort = setting.HTTPPort
- form.AppURL = setting.AppURL
- form.LogRootPath = setting.LogRootPath
-
- // E-mail service settings
- if setting.MailService != nil {
- form.SMTPHost = setting.MailService.Host
- form.SMTPFrom = setting.MailService.From
- form.SMTPUser = setting.MailService.User
- }
- form.RegisterConfirm = setting.Service.RegisterEmailConfirm
- form.MailNotify = setting.Service.EnableNotifyMail
-
- // Server and other services settings
- form.OfflineMode = setting.OfflineMode
- form.DisableGravatar = setting.DisableGravatar
- form.EnableFederatedAvatar = setting.EnableFederatedAvatar
- form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
- form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
- form.DisableRegistration = setting.Service.DisableRegistration
- form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
- form.EnableCaptcha = setting.Service.EnableCaptcha
- form.RequireSignInView = setting.Service.RequireSignInView
- form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
- form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
- form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
- form.NoReplyAddress = setting.Service.NoReplyAddress
- form.PasswordAlgorithm = setting.PasswordHashAlgo
-
- middleware.AssignForm(form, ctx.Data)
- ctx.HTML(http.StatusOK, tplInstall)
-}
-
-// InstallPost response for submit install items
-func InstallPost(ctx *context.Context) {
- form := *web.GetForm(ctx).(*forms.InstallForm)
- var err error
- ctx.Data["CurDbOption"] = form.DbType
-
- if ctx.HasError() {
- if ctx.HasValue("Err_SMTPUser") {
- ctx.Data["Err_SMTP"] = true
- }
- if ctx.HasValue("Err_AdminName") ||
- ctx.HasValue("Err_AdminPasswd") ||
- ctx.HasValue("Err_AdminEmail") {
- ctx.Data["Err_Admin"] = true
- }
-
- ctx.HTML(http.StatusOK, tplInstall)
- return
- }
-
- if _, err = exec.LookPath("git"); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
- return
- }
-
- // Pass basic check, now test configuration.
- // Test database setting.
-
- setting.Database.Type = setting.GetDBTypeByName(form.DbType)
- setting.Database.Host = form.DbHost
- setting.Database.User = form.DbUser
- setting.Database.Passwd = form.DbPasswd
- setting.Database.Name = form.DbName
- setting.Database.Schema = form.DbSchema
- setting.Database.SSLMode = form.SSLMode
- setting.Database.Charset = form.Charset
- setting.Database.Path = form.DbPath
-
- setting.PasswordHashAlgo = form.PasswordAlgorithm
-
- if (setting.Database.Type == "sqlite3") &&
- len(setting.Database.Path) == 0 {
- ctx.Data["Err_DbPath"] = true
- ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, &form)
- return
- }
-
- // Set test engine.
- if err = models.NewTestEngine(); err != nil {
- if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
- ctx.Data["Err_DbType"] = true
- ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.io/en-us/install-from-binary/"), tplInstall, &form)
- } else {
- ctx.Data["Err_DbSetting"] = true
- ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
- }
- return
- }
-
- // Test repository root path.
- form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
- if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
- ctx.Data["Err_RepoRootPath"] = true
- ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
- return
- }
-
- // Test LFS root path if not empty, empty meaning disable LFS
- if form.LFSRootPath != "" {
- form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
- if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
- ctx.Data["Err_LFSRootPath"] = true
- ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
- return
- }
- }
-
- // Test log root path.
- form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
- if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
- ctx.Data["Err_LogRootPath"] = true
- ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
- return
- }
-
- currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser)
- if !match {
- ctx.Data["Err_RunUser"] = true
- ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form)
- return
- }
-
- // Check logic loophole between disable self-registration and no admin account.
- if form.DisableRegistration && len(form.AdminName) == 0 {
- ctx.Data["Err_Services"] = true
- ctx.Data["Err_Admin"] = true
- ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
- return
- }
-
- // Check admin user creation
- if len(form.AdminName) > 0 {
- // Ensure AdminName is valid
- if err := models.IsUsableUsername(form.AdminName); err != nil {
- ctx.Data["Err_Admin"] = true
- ctx.Data["Err_AdminName"] = true
- if models.IsErrNameReserved(err) {
- ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
- return
- } else if models.IsErrNamePatternNotAllowed(err) {
- ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
- return
- }
- // Check Admin email
- if len(form.AdminEmail) == 0 {
- ctx.Data["Err_Admin"] = true
- ctx.Data["Err_AdminEmail"] = true
- ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
- return
- }
- // Check admin password.
- if len(form.AdminPasswd) == 0 {
- ctx.Data["Err_Admin"] = true
- ctx.Data["Err_AdminPasswd"] = true
- ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
- return
- }
- if form.AdminPasswd != form.AdminConfirmPasswd {
- ctx.Data["Err_Admin"] = true
- ctx.Data["Err_AdminPasswd"] = true
- ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
- return
- }
- }
-
- if form.AppURL[len(form.AppURL)-1] != '/' {
- form.AppURL += "/"
- }
-
- // Save settings.
- cfg := ini.Empty()
- isFile, err := util.IsFile(setting.CustomConf)
- if err != nil {
- log.Error("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
- }
- if isFile {
- // Keeps custom settings if there is already something.
- if err = cfg.Append(setting.CustomConf); err != nil {
- log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
- }
- }
- cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type)
- cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
- cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
- cfg.Section("database").Key("USER").SetValue(setting.Database.User)
- cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
- cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
- cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
- cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset)
- cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
- cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
-
- cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
- cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
- cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
- cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
- cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
- cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
- cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
-
- if form.SSHPort == 0 {
- cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
- } else {
- cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
- cfg.Section("server").Key("SSH_PORT").SetValue(fmt.Sprint(form.SSHPort))
- }
-
- if form.LFSRootPath != "" {
- cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
- cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
- var secretKey string
- if secretKey, err = generate.NewJwtSecret(); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
- return
- }
- cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(secretKey)
- } else {
- cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
- }
-
- if len(strings.TrimSpace(form.SMTPHost)) > 0 {
- cfg.Section("mailer").Key("ENABLED").SetValue("true")
- cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
- cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
- cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
- cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
- } else {
- cfg.Section("mailer").Key("ENABLED").SetValue("false")
- }
- cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(fmt.Sprint(form.RegisterConfirm))
- cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify))
-
- cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode))
- cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar))
- cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar))
- cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
- cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
- cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
- cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(fmt.Sprint(form.AllowOnlyExternalRegistration))
- cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(fmt.Sprint(form.EnableCaptcha))
- cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(fmt.Sprint(form.RequireSignInView))
- cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(fmt.Sprint(form.DefaultKeepEmailPrivate))
- cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(fmt.Sprint(form.DefaultAllowCreateOrganization))
- cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(fmt.Sprint(form.DefaultEnableTimetracking))
- cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
-
- cfg.Section("").Key("RUN_MODE").SetValue("prod")
-
- cfg.Section("session").Key("PROVIDER").SetValue("file")
-
- cfg.Section("log").Key("MODE").SetValue("console")
- cfg.Section("log").Key("LEVEL").SetValue(setting.LogLevel.String())
- cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
- cfg.Section("log").Key("ROUTER").SetValue("console")
-
- cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
- var secretKey string
- if secretKey, err = generate.NewSecretKey(); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
- return
- }
- cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
- if len(form.PasswordAlgorithm) > 0 {
- cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
- }
-
- err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
- if err != nil {
- ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
- return
- }
-
- if err = cfg.SaveTo(setting.CustomConf); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
- return
- }
-
- // Re-read settings
- PostInstallInit(ctx)
-
- // Create admin account
- if len(form.AdminName) > 0 {
- u := &models.User{
- Name: form.AdminName,
- Email: form.AdminEmail,
- Passwd: form.AdminPasswd,
- IsAdmin: true,
- IsActive: true,
- }
- if err = models.CreateUser(u); err != nil {
- if !models.IsErrUserAlreadyExist(err) {
- setting.InstallLock = false
- ctx.Data["Err_AdminName"] = true
- ctx.Data["Err_AdminEmail"] = true
- ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
- return
- }
- log.Info("Admin account already exist")
- u, _ = models.GetUserByName(u.Name)
- }
-
- days := 86400 * setting.LogInRememberDays
- ctx.SetCookie(setting.CookieUserName, u.Name, days)
-
- ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
- setting.CookieRememberName, u.Name, days)
-
- // Auto-login for admin
- if err = ctx.Session.Set("uid", u.ID); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
- return
- }
- if err = ctx.Session.Set("uname", u.Name); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
- return
- }
-
- if err = ctx.Session.Release(); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
- return
- }
- }
-
- log.Info("First-time run install finished!")
-
- ctx.Flash.Success(ctx.Tr("install.install_success"))
-
- ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
- ctx.HTML(http.StatusOK, tplPostInstall)
-
- // Now get the http.Server from this request and shut it down
- // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
- srv := ctx.Value(http.ServerContextKey).(*http.Server)
- go func() {
- if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
- log.Error("Unable to shutdown the install server! Error: %v", err)
- }
- }()
-}
--- /dev/null
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// 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 install
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/generate"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/modules/user"
+ "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"
+
+ "gitea.com/go-chi/session"
+ "gopkg.in/ini.v1"
+)
+
+const (
+ // tplInstall template for installation page
+ tplInstall base.TplName = "install"
+ tplPostInstall base.TplName = "post-install"
+)
+
+// Init prepare for rendering installation page
+func Init(next http.Handler) http.Handler {
+ var rnd = templates.HTMLRenderer()
+
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ if setting.InstallLock {
+ resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+ _ = rnd.HTML(resp, 200, string(tplPostInstall), nil)
+ return
+ }
+ var locale = middleware.Locale(resp, req)
+ var startTime = time.Now()
+ var ctx = context.Context{
+ Resp: context.NewResponse(resp),
+ Flash: &middleware.Flash{},
+ Locale: locale,
+ Render: rnd,
+ Session: session.GetSession(req),
+ Data: map[string]interface{}{
+ "Title": locale.Tr("install.install"),
+ "PageIsInstall": true,
+ "DbOptions": setting.SupportedDatabases,
+ "i18n": locale,
+ "Language": locale.Language(),
+ "Lang": locale.Language(),
+ "AllLangs": translation.AllLangs(),
+ "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
+ "PageStartTime": startTime,
+ "TmplLoadTimes": func() string {
+ return time.Since(startTime).String()
+ },
+ "PasswordHashAlgorithms": models.AvailableHashAlgorithms,
+ },
+ }
+ for _, lang := range translation.AllLangs() {
+ if lang.Lang == locale.Language() {
+ ctx.Data["LangName"] = lang.Name
+ break
+ }
+ }
+ ctx.Req = context.WithContext(req, &ctx)
+ next.ServeHTTP(resp, ctx.Req)
+ })
+}
+
+// Install render installation page
+func Install(ctx *context.Context) {
+ form := forms.InstallForm{}
+
+ // Database settings
+ form.DbHost = setting.Database.Host
+ form.DbUser = setting.Database.User
+ form.DbPasswd = setting.Database.Passwd
+ form.DbName = setting.Database.Name
+ form.DbPath = setting.Database.Path
+ form.DbSchema = setting.Database.Schema
+ form.Charset = setting.Database.Charset
+
+ var curDBOption = "MySQL"
+ switch setting.Database.Type {
+ case "postgres":
+ curDBOption = "PostgreSQL"
+ case "mssql":
+ curDBOption = "MSSQL"
+ case "sqlite3":
+ if setting.EnableSQLite3 {
+ curDBOption = "SQLite3"
+ }
+ }
+
+ ctx.Data["CurDbOption"] = curDBOption
+
+ // Application general settings
+ form.AppName = setting.AppName
+ form.RepoRootPath = setting.RepoRootPath
+ form.LFSRootPath = setting.LFS.Path
+
+ // Note(unknown): it's hard for Windows users change a running user,
+ // so just use current one if config says default.
+ if setting.IsWindows && setting.RunUser == "git" {
+ form.RunUser = user.CurrentUsername()
+ } else {
+ form.RunUser = setting.RunUser
+ }
+
+ form.Domain = setting.Domain
+ form.SSHPort = setting.SSH.Port
+ form.HTTPPort = setting.HTTPPort
+ form.AppURL = setting.AppURL
+ form.LogRootPath = setting.LogRootPath
+
+ // E-mail service settings
+ if setting.MailService != nil {
+ form.SMTPHost = setting.MailService.Host
+ form.SMTPFrom = setting.MailService.From
+ form.SMTPUser = setting.MailService.User
+ }
+ form.RegisterConfirm = setting.Service.RegisterEmailConfirm
+ form.MailNotify = setting.Service.EnableNotifyMail
+
+ // Server and other services settings
+ form.OfflineMode = setting.OfflineMode
+ form.DisableGravatar = setting.DisableGravatar
+ form.EnableFederatedAvatar = setting.EnableFederatedAvatar
+ form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
+ form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
+ form.DisableRegistration = setting.Service.DisableRegistration
+ form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
+ form.EnableCaptcha = setting.Service.EnableCaptcha
+ form.RequireSignInView = setting.Service.RequireSignInView
+ form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
+ form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
+ form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
+ form.NoReplyAddress = setting.Service.NoReplyAddress
+ form.PasswordAlgorithm = setting.PasswordHashAlgo
+
+ middleware.AssignForm(form, ctx.Data)
+ ctx.HTML(http.StatusOK, tplInstall)
+}
+
+// SubmitInstall response for submit install items
+func SubmitInstall(ctx *context.Context) {
+ form := *web.GetForm(ctx).(*forms.InstallForm)
+ var err error
+ ctx.Data["CurDbOption"] = form.DbType
+
+ if ctx.HasError() {
+ if ctx.HasValue("Err_SMTPUser") {
+ ctx.Data["Err_SMTP"] = true
+ }
+ if ctx.HasValue("Err_AdminName") ||
+ ctx.HasValue("Err_AdminPasswd") ||
+ ctx.HasValue("Err_AdminEmail") {
+ ctx.Data["Err_Admin"] = true
+ }
+
+ ctx.HTML(http.StatusOK, tplInstall)
+ return
+ }
+
+ if _, err = exec.LookPath("git"); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
+ return
+ }
+
+ // Pass basic check, now test configuration.
+ // Test database setting.
+
+ setting.Database.Type = setting.GetDBTypeByName(form.DbType)
+ setting.Database.Host = form.DbHost
+ setting.Database.User = form.DbUser
+ setting.Database.Passwd = form.DbPasswd
+ setting.Database.Name = form.DbName
+ setting.Database.Schema = form.DbSchema
+ setting.Database.SSLMode = form.SSLMode
+ setting.Database.Charset = form.Charset
+ setting.Database.Path = form.DbPath
+
+ setting.PasswordHashAlgo = form.PasswordAlgorithm
+
+ if (setting.Database.Type == "sqlite3") &&
+ len(setting.Database.Path) == 0 {
+ ctx.Data["Err_DbPath"] = true
+ ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, &form)
+ return
+ }
+
+ // Set test engine.
+ if err = models.NewTestEngine(); err != nil {
+ if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
+ ctx.Data["Err_DbType"] = true
+ ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.io/en-us/install-from-binary/"), tplInstall, &form)
+ } else {
+ ctx.Data["Err_DbSetting"] = true
+ ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
+ }
+ return
+ }
+
+ // Test repository root path.
+ form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
+ if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
+ ctx.Data["Err_RepoRootPath"] = true
+ ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
+ return
+ }
+
+ // Test LFS root path if not empty, empty meaning disable LFS
+ if form.LFSRootPath != "" {
+ form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
+ if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
+ ctx.Data["Err_LFSRootPath"] = true
+ ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
+ return
+ }
+ }
+
+ // Test log root path.
+ form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
+ if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
+ ctx.Data["Err_LogRootPath"] = true
+ ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
+ return
+ }
+
+ currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser)
+ if !match {
+ ctx.Data["Err_RunUser"] = true
+ ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form)
+ return
+ }
+
+ // Check logic loophole between disable self-registration and no admin account.
+ if form.DisableRegistration && len(form.AdminName) == 0 {
+ ctx.Data["Err_Services"] = true
+ ctx.Data["Err_Admin"] = true
+ ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
+ return
+ }
+
+ // Check admin user creation
+ if len(form.AdminName) > 0 {
+ // Ensure AdminName is valid
+ if err := models.IsUsableUsername(form.AdminName); err != nil {
+ ctx.Data["Err_Admin"] = true
+ ctx.Data["Err_AdminName"] = true
+ if models.IsErrNameReserved(err) {
+ ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
+ return
+ } else if models.IsErrNamePatternNotAllowed(err) {
+ ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
+ return
+ }
+ ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
+ return
+ }
+ // Check Admin email
+ if len(form.AdminEmail) == 0 {
+ ctx.Data["Err_Admin"] = true
+ ctx.Data["Err_AdminEmail"] = true
+ ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
+ return
+ }
+ // Check admin password.
+ if len(form.AdminPasswd) == 0 {
+ ctx.Data["Err_Admin"] = true
+ ctx.Data["Err_AdminPasswd"] = true
+ ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
+ return
+ }
+ if form.AdminPasswd != form.AdminConfirmPasswd {
+ ctx.Data["Err_Admin"] = true
+ ctx.Data["Err_AdminPasswd"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
+ return
+ }
+ }
+
+ if form.AppURL[len(form.AppURL)-1] != '/' {
+ form.AppURL += "/"
+ }
+
+ // Save settings.
+ cfg := ini.Empty()
+ isFile, err := util.IsFile(setting.CustomConf)
+ if err != nil {
+ log.Error("Unable to check if %s is a file. Error: %v", setting.CustomConf, err)
+ }
+ if isFile {
+ // Keeps custom settings if there is already something.
+ if err = cfg.Append(setting.CustomConf); err != nil {
+ log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
+ }
+ }
+ cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type)
+ cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
+ cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
+ cfg.Section("database").Key("USER").SetValue(setting.Database.User)
+ cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
+ cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
+ cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
+ cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset)
+ cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
+ cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
+
+ cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
+ cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
+ cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
+ cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
+ cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
+ cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
+ cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
+
+ if form.SSHPort == 0 {
+ cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
+ } else {
+ cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
+ cfg.Section("server").Key("SSH_PORT").SetValue(fmt.Sprint(form.SSHPort))
+ }
+
+ if form.LFSRootPath != "" {
+ cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
+ cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
+ var secretKey string
+ if secretKey, err = generate.NewJwtSecret(); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
+ return
+ }
+ cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(secretKey)
+ } else {
+ cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
+ }
+
+ if len(strings.TrimSpace(form.SMTPHost)) > 0 {
+ cfg.Section("mailer").Key("ENABLED").SetValue("true")
+ cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
+ cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
+ cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
+ cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
+ } else {
+ cfg.Section("mailer").Key("ENABLED").SetValue("false")
+ }
+ cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(fmt.Sprint(form.RegisterConfirm))
+ cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify))
+
+ cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode))
+ cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar))
+ cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar))
+ cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
+ cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
+ cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
+ cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(fmt.Sprint(form.AllowOnlyExternalRegistration))
+ cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(fmt.Sprint(form.EnableCaptcha))
+ cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(fmt.Sprint(form.RequireSignInView))
+ cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(fmt.Sprint(form.DefaultKeepEmailPrivate))
+ cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(fmt.Sprint(form.DefaultAllowCreateOrganization))
+ cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(fmt.Sprint(form.DefaultEnableTimetracking))
+ cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
+
+ cfg.Section("").Key("RUN_MODE").SetValue("prod")
+
+ cfg.Section("session").Key("PROVIDER").SetValue("file")
+
+ cfg.Section("log").Key("MODE").SetValue("console")
+ cfg.Section("log").Key("LEVEL").SetValue(setting.LogLevel.String())
+ cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
+ cfg.Section("log").Key("ROUTER").SetValue("console")
+
+ cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
+ var secretKey string
+ if secretKey, err = generate.NewSecretKey(); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
+ return
+ }
+ cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
+ if len(form.PasswordAlgorithm) > 0 {
+ cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
+ }
+
+ err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
+ if err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
+
+ if err = cfg.SaveTo(setting.CustomConf); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
+
+ // Re-read settings
+ ReloadSettings(ctx)
+
+ // Create admin account
+ if len(form.AdminName) > 0 {
+ u := &models.User{
+ Name: form.AdminName,
+ Email: form.AdminEmail,
+ Passwd: form.AdminPasswd,
+ IsAdmin: true,
+ IsActive: true,
+ }
+ if err = models.CreateUser(u); err != nil {
+ if !models.IsErrUserAlreadyExist(err) {
+ setting.InstallLock = false
+ ctx.Data["Err_AdminName"] = true
+ ctx.Data["Err_AdminEmail"] = true
+ ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
+ return
+ }
+ log.Info("Admin account already exist")
+ u, _ = models.GetUserByName(u.Name)
+ }
+
+ days := 86400 * setting.LogInRememberDays
+ ctx.SetCookie(setting.CookieUserName, u.Name, days)
+
+ ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
+ setting.CookieRememberName, u.Name, days)
+
+ // Auto-login for admin
+ if err = ctx.Session.Set("uid", u.ID); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
+ if err = ctx.Session.Set("uname", u.Name); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
+
+ if err = ctx.Session.Release(); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
+ }
+
+ log.Info("First-time run install finished!")
+
+ ctx.Flash.Success(ctx.Tr("install.install_success"))
+
+ ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+ ctx.HTML(http.StatusOK, tplPostInstall)
+
+ // Now get the http.Server from this request and shut it down
+ // NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
+ srv := ctx.Value(http.ServerContextKey).(*http.Server)
+ go func() {
+ if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
+ log.Error("Unable to shutdown the install server! Error: %v", err)
+ }
+ }()
+}
--- /dev/null
+// 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 install
+
+import (
+ "fmt"
+ "net/http"
+ "path"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/public"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/forms"
+
+ "gitea.com/go-chi/session"
+)
+
+type dataStore map[string]interface{}
+
+func (d *dataStore) GetData() map[string]interface{} {
+ return *d
+}
+
+func installRecovery() 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() {
+ // Why we need this? The first recover will try to render a beautiful
+ // error page for user, but the process can still panic again, then
+ // we have to just recover twice and send a simple error page that
+ // should not panic any more.
+ defer func() {
+ if err := recover(); err != nil {
+ combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+ log.Error(combinedErr)
+ if setting.IsProd() {
+ http.Error(w, http.StatusText(500), 500)
+ } else {
+ http.Error(w, combinedErr, 500)
+ }
+ }
+ }()
+
+ if err := recover(); err != nil {
+ combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+ log.Error("%v", combinedErr)
+
+ lc := middleware.Locale(w, req)
+ var store = dataStore{
+ "Language": lc.Language(),
+ "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
+ "i18n": lc,
+ "SignedUserID": int64(0),
+ "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)
+ })
+ }
+}
+
+// Routes registers the install routes
+func Routes() *web.Route {
+ r := web.NewRoute()
+ for _, middle := range common.Middlewares() {
+ r.Use(middle)
+ }
+
+ r.Use(public.AssetsHandler(&public.Options{
+ Directory: path.Join(setting.StaticRootPath, "public"),
+ Prefix: "/assets",
+ }))
+
+ r.Use(session.Sessioner(session.Options{
+ Provider: setting.SessionConfig.Provider,
+ ProviderConfig: setting.SessionConfig.ProviderConfig,
+ CookieName: setting.SessionConfig.CookieName,
+ CookiePath: setting.SessionConfig.CookiePath,
+ Gclifetime: setting.SessionConfig.Gclifetime,
+ Maxlifetime: setting.SessionConfig.Maxlifetime,
+ Secure: setting.SessionConfig.Secure,
+ SameSite: setting.SessionConfig.SameSite,
+ Domain: setting.SessionConfig.Domain,
+ }))
+
+ r.Use(installRecovery())
+ r.Use(Init)
+ r.Get("/", Install)
+ r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
+ r.NotFound(func(w http.ResponseWriter, req *http.Request) {
+ http.Redirect(w, req, setting.AppURL, http.StatusFound)
+ })
+ return r
+}
--- /dev/null
+// 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 install
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/svg"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/routers/common"
+)
+
+// PreloadSettings preloads the configuration to check if we need to run install
+func PreloadSettings(ctx context.Context) bool {
+ setting.NewContext()
+ if !setting.InstallLock {
+ log.Trace("AppPath: %s", setting.AppPath)
+ log.Trace("AppWorkPath: %s", setting.AppWorkPath)
+ log.Trace("Custom path: %s", setting.CustomPath)
+ log.Trace("Log path: %s", setting.LogRootPath)
+ log.Trace("Preparing to run install page")
+ translation.InitLocales()
+ if setting.EnableSQLite3 {
+ log.Info("SQLite3 Supported")
+ }
+ setting.InitDBConfig()
+ svg.Init()
+ }
+
+ return !setting.InstallLock
+}
+
+// ReloadSettings rereads the settings and starts up the database
+func ReloadSettings(ctx context.Context) {
+ setting.NewContext()
+ setting.InitDBConfig()
+ if setting.InstallLock {
+ if err := common.InitDBEngine(ctx); err == nil {
+ log.Info("ORM engine initialization successful!")
+ } else {
+ log.Fatal("ORM engine initialization failed: %v", err)
+ }
+ svg.Init()
+ }
+}
+++ /dev/null
-// 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 routers
-
-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)
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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 + "/")
- }
-}
+++ /dev/null
-// 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())
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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/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)
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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"
-)
-
-// 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 = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
- ctx.ServerError("ServeData", err)
- return
- }
-}
+++ /dev/null
-// 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">​</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())
-}
+++ /dev/null
-// 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))
-}
+++ /dev/null
-// 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
- }
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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"
- "io"
- "path"
- "path/filepath"
- "strings"
-
- "code.gitea.io/gitea/modules/charset"
- "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/modules/setting"
- "code.gitea.io/gitea/modules/typesniffer"
-)
-
-// ServeData download file from io.Reader
-func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
- buf := make([]byte, 1024)
- n, err := reader.Read(buf)
- if err != nil && err != io.EOF {
- return err
- }
- if n >= 0 {
- buf = buf[:n]
- }
-
- ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
-
- if size >= 0 {
- ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
- } else {
- log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
- }
- name = path.Base(name)
-
- // Google Chrome dislike commas in filenames, so let's change it to a space
- name = strings.ReplaceAll(name, ",", " ")
-
- st := typesniffer.DetectContentType(buf)
-
- if st.IsText() || ctx.QueryBool("render") {
- cs, err := charset.DetectEncoding(buf)
- if err != nil {
- log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
- cs = "utf-8"
- }
- ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
- } else {
- ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
-
- if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
- ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
- if st.IsSvgImage() {
- ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
- ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
- ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
- }
- } else {
- ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
- if setting.MimeTypeMap.Enabled {
- fileExtension := strings.ToLower(filepath.Ext(name))
- if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
- ctx.Resp.Header().Set("Content-Type", mimetype)
- }
- }
- }
- }
-
- _, err = ctx.Resp.Write(buf)
- if err != nil {
- return err
- }
- _, err = io.Copy(ctx.Resp, reader)
- return err
-}
-
-// ServeBlob download a git.Blob
-func ServeBlob(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
- }
- defer func() {
- if err = dataRc.Close(); err != nil {
- log.Error("ServeBlob: Close: %v", err)
- }
- }()
-
- return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
-}
-
-// 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 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 ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
- }
- if err = dataRc.Close(); err != nil {
- log.Error("ServeBlobOrLFS: Close: %v", err)
- }
- closed = true
-
- return 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 = 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 = 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)
- }
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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)
- }
-}
+++ /dev/null
-// 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")
- }
-}
+++ /dev/null
-// 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 (
- "bytes"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "path"
- "strconv"
- "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/git"
- 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"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/upload"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- comment_service "code.gitea.io/gitea/services/comments"
- "code.gitea.io/gitea/services/forms"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
-
- "github.com/unknwon/com"
-)
-
-const (
- tplAttachment base.TplName = "repo/issue/view_content/attachments"
-
- tplIssues base.TplName = "repo/issue/list"
- tplIssueNew base.TplName = "repo/issue/new"
- tplIssueChoose base.TplName = "repo/issue/choose"
- tplIssueView base.TplName = "repo/issue/view"
-
- tplReactions base.TplName = "repo/issue/view_content/reactions"
-
- issueTemplateKey = "IssueTemplate"
- issueTemplateTitleKey = "IssueTemplateTitle"
-)
-
-var (
- // IssueTemplateCandidates issue templates
- IssueTemplateCandidates = []string{
- "ISSUE_TEMPLATE.md",
- "issue_template.md",
- ".gitea/ISSUE_TEMPLATE.md",
- ".gitea/issue_template.md",
- ".github/ISSUE_TEMPLATE.md",
- ".github/issue_template.md",
- }
-)
-
-// MustAllowUserComment checks to make sure if an issue is locked.
-// If locked and user has permissions to write to the repository,
-// then the comment is allowed, else it is blocked
-func MustAllowUserComment(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- if ctx.Written() {
- return
- }
-
- if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
- ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
- ctx.Redirect(issue.HTMLURL())
- return
- }
-}
-
-// MustEnableIssues check if repository enable internal issues
-func MustEnableIssues(ctx *context.Context) {
- if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
- !ctx.Repo.CanRead(models.UnitTypeExternalTracker) {
- ctx.NotFound("MustEnableIssues", nil)
- return
- }
-
- unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
- if err == nil {
- ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
- return
- }
-}
-
-// MustAllowPulls check if repository enable pull requests and user have right to do that
-func MustAllowPulls(ctx *context.Context) {
- if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(models.UnitTypePullRequests) {
- ctx.NotFound("MustAllowPulls", nil)
- return
- }
-
- // User can send pull request if owns a forked repository.
- if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) {
- ctx.Repo.PullRequest.Allowed = true
- ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName
- }
-}
-
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
- var err error
- viewType := ctx.Query("type")
- sortType := ctx.Query("sort")
- types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
- if !util.IsStringInSlice(viewType, types, true) {
- viewType = "all"
- }
-
- var (
- assigneeID = ctx.QueryInt64("assignee")
- posterID int64
- mentionedID int64
- reviewRequestedID int64
- forceEmpty bool
- )
-
- if ctx.IsSigned {
- switch viewType {
- case "created_by":
- posterID = ctx.User.ID
- case "mentioned":
- mentionedID = ctx.User.ID
- case "assigned":
- assigneeID = ctx.User.ID
- case "review_requested":
- reviewRequestedID = ctx.User.ID
- }
- }
-
- repo := ctx.Repo.Repository
- var labelIDs []int64
- selectLabels := ctx.Query("labels")
- if len(selectLabels) > 0 && selectLabels != "0" {
- labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
- if err != nil {
- ctx.ServerError("StringsToInt64s", err)
- return
- }
- }
-
- keyword := strings.Trim(ctx.Query("q"), " ")
- if bytes.Contains([]byte(keyword), []byte{0x00}) {
- keyword = ""
- }
-
- var issueIDs []int64
- if len(keyword) > 0 {
- issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
- if err != nil {
- ctx.ServerError("issueIndexer.Search", err)
- return
- }
- if len(issueIDs) == 0 {
- forceEmpty = true
- }
- }
-
- var issueStats *models.IssueStats
- if forceEmpty {
- issueStats = &models.IssueStats{}
- } else {
- issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
- RepoID: repo.ID,
- Labels: selectLabels,
- MilestoneID: milestoneID,
- AssigneeID: assigneeID,
- MentionedID: mentionedID,
- PosterID: posterID,
- ReviewRequestedID: reviewRequestedID,
- IsPull: isPullOption,
- IssueIDs: issueIDs,
- })
- if err != nil {
- ctx.ServerError("GetIssueStats", err)
- return
- }
- }
-
- isShowClosed := ctx.Query("state") == "closed"
- // if open issues are zero and close don't, use closed as default
- if len(ctx.Query("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
- isShowClosed = true
- }
-
- page := ctx.QueryInt("page")
- if page <= 1 {
- page = 1
- }
-
- var total int
- if !isShowClosed {
- total = int(issueStats.OpenCount)
- } else {
- total = int(issueStats.ClosedCount)
- }
- pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
-
- var mileIDs []int64
- if milestoneID > 0 {
- mileIDs = []int64{milestoneID}
- }
-
- var issues []*models.Issue
- if forceEmpty {
- issues = []*models.Issue{}
- } else {
- issues, err = models.Issues(&models.IssuesOptions{
- ListOptions: models.ListOptions{
- Page: pager.Paginater.Current(),
- PageSize: setting.UI.IssuePagingNum,
- },
- RepoIDs: []int64{repo.ID},
- AssigneeID: assigneeID,
- PosterID: posterID,
- MentionedID: mentionedID,
- ReviewRequestedID: reviewRequestedID,
- MilestoneIDs: mileIDs,
- ProjectID: projectID,
- IsClosed: util.OptionalBoolOf(isShowClosed),
- IsPull: isPullOption,
- LabelIDs: labelIDs,
- SortType: sortType,
- IssueIDs: issueIDs,
- })
- if err != nil {
- ctx.ServerError("Issues", err)
- return
- }
- }
-
- var issueList = models.IssueList(issues)
- approvalCounts, err := issueList.GetApprovalCounts()
- if err != nil {
- ctx.ServerError("ApprovalCounts", err)
- return
- }
-
- // Get posters.
- for i := range issues {
- // Check read status
- if !ctx.IsSigned {
- issues[i].IsRead = true
- } else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
- ctx.ServerError("GetIsRead", err)
- return
- }
- }
-
- commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
- if err != nil {
- ctx.ServerError("GetIssuesLastCommitStatus", err)
- return
- }
-
- ctx.Data["Issues"] = issues
- ctx.Data["CommitStatus"] = commitStatus
-
- // Get assignees.
- ctx.Data["Assignees"], err = repo.GetAssignees()
- if err != nil {
- ctx.ServerError("GetAssignees", err)
- return
- }
-
- handleTeamMentions(ctx)
- if ctx.Written() {
- return
- }
-
- labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByRepoID", err)
- return
- }
-
- if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByOrgID", err)
- return
- }
-
- ctx.Data["OrgLabels"] = orgLabels
- labels = append(labels, orgLabels...)
- }
-
- for _, l := range labels {
- l.LoadSelectedLabelsAfterClick(labelIDs)
- }
- ctx.Data["Labels"] = labels
- ctx.Data["NumLabels"] = len(labels)
-
- if ctx.QueryInt64("assignee") == 0 {
- assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
- }
-
- ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
- issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
-
- 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["IssueStats"] = issueStats
- ctx.Data["SelLabelIDs"] = labelIDs
- ctx.Data["SelectLabels"] = selectLabels
- ctx.Data["ViewType"] = viewType
- ctx.Data["SortType"] = sortType
- ctx.Data["MilestoneID"] = milestoneID
- ctx.Data["AssigneeID"] = assigneeID
- ctx.Data["IsShowClosed"] = isShowClosed
- ctx.Data["Keyword"] = keyword
- if isShowClosed {
- ctx.Data["State"] = "closed"
- } else {
- ctx.Data["State"] = "open"
- }
-
- pager.AddParam(ctx, "q", "Keyword")
- pager.AddParam(ctx, "type", "ViewType")
- 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
-}
-
-// Issues render issues page
-func Issues(ctx *context.Context) {
- isPullList := ctx.Params(":type") == "pulls"
- if isPullList {
- MustAllowPulls(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Title"] = ctx.Tr("repo.pulls")
- ctx.Data["PageIsPullList"] = true
- } else {
- MustEnableIssues(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Title"] = ctx.Tr("repo.issues")
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- }
-
- issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList))
- if ctx.Written() {
- return
- }
-
- var err error
- // Get milestones
- ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{
- RepoID: ctx.Repo.Repository.ID,
- State: api.StateType(ctx.Query("state")),
- })
- if err != nil {
- ctx.ServerError("GetAllRepoMilestones", err)
- return
- }
-
- ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
-
- ctx.HTML(http.StatusOK, tplIssues)
-}
-
-// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
-func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
- var err error
- ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
- RepoID: repo.ID,
- State: api.StateOpen,
- })
- if err != nil {
- ctx.ServerError("GetMilestones", err)
- return
- }
- ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
- RepoID: repo.ID,
- State: api.StateClosed,
- })
- if err != nil {
- ctx.ServerError("GetMilestones", err)
- return
- }
-
- ctx.Data["Assignees"], err = repo.GetAssignees()
- if err != nil {
- ctx.ServerError("GetAssignees", err)
- return
- }
-
- handleTeamMentions(ctx)
- if ctx.Written() {
- return
- }
-}
-
-func retrieveProjects(ctx *context.Context, repo *models.Repository) {
-
- var err error
-
- ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
- RepoID: repo.ID,
- Page: -1,
- IsClosed: util.OptionalBoolFalse,
- Type: models.ProjectTypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
-
- ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
- RepoID: repo.ID,
- Page: -1,
- IsClosed: util.OptionalBoolTrue,
- Type: models.ProjectTypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
-}
-
-// repoReviewerSelection items to bee shown
-type repoReviewerSelection struct {
- IsTeam bool
- Team *models.Team
- User *models.User
- Review *models.Review
- CanChange bool
- Checked bool
- ItemID int64
-}
-
-// RetrieveRepoReviewers find all reviewers of a repository
-func RetrieveRepoReviewers(ctx *context.Context, repo *models.Repository, issue *models.Issue, canChooseReviewer bool) {
- ctx.Data["CanChooseReviewer"] = canChooseReviewer
-
- originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
- if err != nil {
- ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
- return
- }
- ctx.Data["OriginalReviews"] = originalAuthorReviews
-
- reviews, err := models.GetReviewersByIssueID(issue.ID)
- if err != nil {
- ctx.ServerError("GetReviewersByIssueID", err)
- return
- }
-
- if len(reviews) == 0 && !canChooseReviewer {
- return
- }
-
- var (
- pullReviews []*repoReviewerSelection
- reviewersResult []*repoReviewerSelection
- teamReviewersResult []*repoReviewerSelection
- teamReviewers []*models.Team
- reviewers []*models.User
- )
-
- if canChooseReviewer {
- posterID := issue.PosterID
- if issue.OriginalAuthorID > 0 {
- posterID = 0
- }
-
- reviewers, err = repo.GetReviewers(ctx.User.ID, posterID)
- if err != nil {
- ctx.ServerError("GetReviewers", err)
- return
- }
-
- teamReviewers, err = repo.GetReviewerTeams()
- if err != nil {
- ctx.ServerError("GetReviewerTeams", err)
- return
- }
-
- if len(reviewers) > 0 {
- reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
- }
-
- if len(teamReviewers) > 0 {
- teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
- }
- }
-
- pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
-
- for _, review := range reviews {
- tmp := &repoReviewerSelection{
- Checked: review.Type == models.ReviewTypeRequest,
- Review: review,
- ItemID: review.ReviewerID,
- }
- if review.ReviewerTeamID > 0 {
- tmp.IsTeam = true
- tmp.ItemID = -review.ReviewerTeamID
- }
-
- if ctx.Repo.IsAdmin() {
- // Admin can dismiss or re-request any review requests
- tmp.CanChange = true
- } else if ctx.User != nil && ctx.User.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
- // A user can refuse review requests
- tmp.CanChange = true
- } else if (canChooseReviewer || (ctx.User != nil && ctx.User.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
- ctx.User.ID != review.ReviewerID {
- // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
- tmp.CanChange = true
- }
-
- pullReviews = append(pullReviews, tmp)
-
- if canChooseReviewer {
- if tmp.IsTeam {
- teamReviewersResult = append(teamReviewersResult, tmp)
- } else {
- reviewersResult = append(reviewersResult, tmp)
- }
- }
- }
-
- if len(pullReviews) > 0 {
- // Drop all non-existing users and teams from the reviews
- currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
- for _, item := range pullReviews {
- if item.Review.ReviewerID > 0 {
- if err = item.Review.LoadReviewer(); err != nil {
- if models.IsErrUserNotExist(err) {
- continue
- }
- ctx.ServerError("LoadReviewer", err)
- return
- }
- item.User = item.Review.Reviewer
- } else if item.Review.ReviewerTeamID > 0 {
- if err = item.Review.LoadReviewerTeam(); err != nil {
- if models.IsErrTeamNotExist(err) {
- continue
- }
- ctx.ServerError("LoadReviewerTeam", err)
- return
- }
- item.Team = item.Review.ReviewerTeam
- } else {
- continue
- }
-
- currentPullReviewers = append(currentPullReviewers, item)
- }
- ctx.Data["PullReviewers"] = currentPullReviewers
- }
-
- if canChooseReviewer && reviewersResult != nil {
- preadded := len(reviewersResult)
- for _, reviewer := range reviewers {
- found := false
- reviewAddLoop:
- for _, tmp := range reviewersResult[:preadded] {
- if tmp.ItemID == reviewer.ID {
- tmp.User = reviewer
- found = true
- break reviewAddLoop
- }
- }
-
- if found {
- continue
- }
-
- reviewersResult = append(reviewersResult, &repoReviewerSelection{
- IsTeam: false,
- CanChange: true,
- User: reviewer,
- ItemID: reviewer.ID,
- })
- }
-
- ctx.Data["Reviewers"] = reviewersResult
- }
-
- if canChooseReviewer && teamReviewersResult != nil {
- preadded := len(teamReviewersResult)
- for _, team := range teamReviewers {
- found := false
- teamReviewAddLoop:
- for _, tmp := range teamReviewersResult[:preadded] {
- if tmp.ItemID == -team.ID {
- tmp.Team = team
- found = true
- break teamReviewAddLoop
- }
- }
-
- if found {
- continue
- }
-
- teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
- IsTeam: true,
- CanChange: true,
- Team: team,
- ItemID: -team.ID,
- })
- }
-
- ctx.Data["TeamReviewers"] = teamReviewersResult
- }
-}
-
-// RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull bool) []*models.Label {
- if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
- return nil
- }
-
- labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByRepoID", err)
- return nil
- }
- ctx.Data["Labels"] = labels
- if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
- if err != nil {
- return nil
- }
-
- ctx.Data["OrgLabels"] = orgLabels
- labels = append(labels, orgLabels...)
- }
-
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- if ctx.Written() {
- return nil
- }
-
- retrieveProjects(ctx, repo)
- if ctx.Written() {
- return nil
- }
-
- brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
- if err != nil {
- ctx.ServerError("GetBranches", err)
- return nil
- }
- ctx.Data["Branches"] = brs
-
- // Contains true if the user can create issue dependencies
- ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
-
- return labels
-}
-
-func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
- var bytes []byte
-
- if ctx.Repo.Commit == nil {
- var err error
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
- return "", false
- }
- }
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
- if err != nil {
- return "", false
- }
- if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
- return "", false
- }
- r, err := entry.Blob().DataAsync()
- if err != nil {
- return "", false
- }
- defer r.Close()
- bytes, err = ioutil.ReadAll(r)
- if err != nil {
- return "", false
- }
- return string(bytes), true
-}
-
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs []string, possibleFiles []string) {
- templateCandidates := make([]string, 0, len(possibleFiles))
- if ctx.Query("template") != "" {
- for _, dirName := range possibleDirs {
- templateCandidates = append(templateCandidates, path.Join(dirName, ctx.Query("template")))
- }
- }
- templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
- for _, filename := range templateCandidates {
- templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
- if found {
- var meta api.IssueTemplate
- templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
- if err != nil {
- log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
- ctx.Data[ctxDataKey] = templateContent
- return
- }
- ctx.Data[issueTemplateTitleKey] = meta.Title
- ctx.Data[ctxDataKey] = templateBody
- labelIDs := make([]string, 0, len(meta.Labels))
- if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", models.ListOptions{}); err == nil {
- ctx.Data["Labels"] = repoLabels
- if ctx.Repo.Owner.IsOrganization() {
- if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{}); err == nil {
- ctx.Data["OrgLabels"] = orgLabels
- repoLabels = append(repoLabels, orgLabels...)
- }
- }
-
- for _, metaLabel := range meta.Labels {
- for _, repoLabel := range repoLabels {
- if strings.EqualFold(repoLabel.Name, metaLabel) {
- repoLabel.IsChecked = true
- labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID))
- break
- }
- }
- }
- }
- ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
- ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
- return
- }
- }
-}
-
-// NewIssue render creating issue page
-func NewIssue(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.issues.new")
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["RequireSimpleMDE"] = true
- ctx.Data["RequireTribute"] = true
- ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
- title := ctx.Query("title")
- ctx.Data["TitleQuery"] = title
- body := ctx.Query("body")
- ctx.Data["BodyQuery"] = body
-
- ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
- ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
- upload.AddUploadContext(ctx, "comment")
-
- milestoneID := ctx.QueryInt64("milestone")
- if milestoneID > 0 {
- milestone, err := models.GetMilestoneByID(milestoneID)
- if err != nil {
- log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
- } else {
- ctx.Data["milestone_id"] = milestoneID
- ctx.Data["Milestone"] = milestone
- }
- }
-
- projectID := ctx.QueryInt64("project")
- if projectID > 0 {
- project, err := models.GetProjectByID(projectID)
- if err != nil {
- log.Error("GetProjectByID: %d: %v", projectID, err)
- } else if project.RepoID != ctx.Repo.Repository.ID {
- log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
- } else {
- ctx.Data["project_id"] = projectID
- ctx.Data["Project"] = project
- }
-
- }
-
- RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
- setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
- if ctx.Written() {
- return
- }
-
- ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeIssues)
-
- ctx.HTML(http.StatusOK, tplIssueNew)
-}
-
-// NewIssueChooseTemplate render creating issue from template page
-func NewIssueChooseTemplate(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.issues.new")
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["milestone"] = ctx.QueryInt64("milestone")
-
- issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
- ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
- ctx.Data["IssueTemplates"] = issueTemplates
-
- ctx.HTML(http.StatusOK, tplIssueChoose)
-}
-
-// ValidateRepoMetas check and returns repository's meta informations
-func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
- var (
- repo = ctx.Repo.Repository
- err error
- )
-
- labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
- if ctx.Written() {
- return nil, nil, 0, 0
- }
-
- var labelIDs []int64
- hasSelected := false
- // Check labels.
- if len(form.LabelIDs) > 0 {
- labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
- if err != nil {
- return nil, nil, 0, 0
- }
- labelIDMark := base.Int64sToMap(labelIDs)
-
- for i := range labels {
- if labelIDMark[labels[i].ID] {
- labels[i].IsChecked = true
- hasSelected = true
- }
- }
- }
-
- ctx.Data["Labels"] = labels
- ctx.Data["HasSelectedLabel"] = hasSelected
- ctx.Data["label_ids"] = form.LabelIDs
-
- // Check milestone.
- milestoneID := form.MilestoneID
- if milestoneID > 0 {
- ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
- if err != nil {
- ctx.ServerError("GetMilestoneByID", err)
- return nil, nil, 0, 0
- }
- ctx.Data["milestone_id"] = milestoneID
- }
-
- if form.ProjectID > 0 {
- p, err := models.GetProjectByID(form.ProjectID)
- if err != nil {
- ctx.ServerError("GetProjectByID", err)
- return nil, nil, 0, 0
- }
- if p.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
- return nil, nil, 0, 0
- }
-
- ctx.Data["Project"] = p
- ctx.Data["project_id"] = form.ProjectID
- }
-
- // Check assignees
- var assigneeIDs []int64
- if len(form.AssigneeIDs) > 0 {
- assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
- if err != nil {
- return nil, nil, 0, 0
- }
-
- // Check if the passed assignees actually exists and is assignable
- for _, aID := range assigneeIDs {
- assignee, err := models.GetUserByID(aID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return nil, nil, 0, 0
- }
-
- valid, err := models.CanBeAssigned(assignee, repo, isPull)
- if err != nil {
- ctx.ServerError("CanBeAssigned", err)
- return nil, nil, 0, 0
- }
-
- if !valid {
- ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
- return nil, nil, 0, 0
- }
- }
- }
-
- // Keep the old assignee id thingy for compatibility reasons
- if form.AssigneeID > 0 {
- assigneeIDs = append(assigneeIDs, form.AssigneeID)
- }
-
- return labelIDs, assigneeIDs, milestoneID, form.ProjectID
-}
-
-// NewIssuePost response for creating new issue
-func NewIssuePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateIssueForm)
- ctx.Data["Title"] = ctx.Tr("repo.issues.new")
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["RequireSimpleMDE"] = true
- ctx.Data["ReadOnly"] = false
- ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
- ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
- upload.AddUploadContext(ctx, "comment")
-
- var (
- repo = ctx.Repo.Repository
- attachments []string
- )
-
- labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
- if ctx.Written() {
- return
- }
-
- if setting.Attachment.Enabled {
- attachments = form.Files
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplIssueNew)
- return
- }
-
- if util.IsEmptyString(form.Title) {
- ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplIssueNew, form)
- return
- }
-
- issue := &models.Issue{
- RepoID: repo.ID,
- Title: form.Title,
- PosterID: ctx.User.ID,
- Poster: ctx.User,
- MilestoneID: milestoneID,
- Content: form.Content,
- Ref: form.Ref,
- }
-
- if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
- ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
- }
- ctx.ServerError("NewIssue", err)
- return
- }
-
- if projectID > 0 {
- if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
- ctx.ServerError("ChangeProjectAssign", err)
- return
- }
- }
-
- log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
- ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
-}
-
-// commentTag returns the CommentTag for a comment in/with the given repo, poster and issue
-func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) {
- perm, err := models.GetUserRepoPermission(repo, poster)
- if err != nil {
- return models.CommentTagNone, err
- }
- if perm.IsOwner() {
- if !poster.IsAdmin {
- return models.CommentTagOwner, nil
- }
-
- ok, err := models.IsUserRealRepoAdmin(repo, poster)
- if err != nil {
- return models.CommentTagNone, err
- }
-
- if ok {
- return models.CommentTagOwner, nil
- }
-
- if ok, err = repo.IsCollaborator(poster.ID); ok && err == nil {
- return models.CommentTagWriter, nil
- }
-
- return models.CommentTagNone, err
- }
-
- if perm.CanWrite(models.UnitTypeCode) {
- return models.CommentTagWriter, nil
- }
-
- return models.CommentTagNone, nil
-}
-
-func getBranchData(ctx *context.Context, issue *models.Issue) {
- ctx.Data["BaseBranch"] = nil
- ctx.Data["HeadBranch"] = nil
- ctx.Data["HeadUserName"] = nil
- ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
- if issue.IsPull {
- pull := issue.PullRequest
- ctx.Data["BaseBranch"] = pull.BaseBranch
- ctx.Data["HeadBranch"] = pull.HeadBranch
- ctx.Data["HeadUserName"] = pull.MustHeadUserName()
- }
-}
-
-// ViewIssue render issue view page
-func ViewIssue(ctx *context.Context) {
- if ctx.Params(":type") == "issues" {
- // If issue was requested we check if repo has external tracker and redirect
- extIssueUnit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
- if err == nil && extIssueUnit != nil {
- if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
- metas := ctx.Repo.Repository.ComposeMetas()
- metas["index"] = ctx.Params(":index")
- ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas))
- return
- }
- } else if err != nil && !models.IsErrUnitTypeNotExist(err) {
- ctx.ServerError("GetUnit", err)
- return
- }
- }
-
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
- if err != nil {
- if models.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
- } else {
- ctx.ServerError("GetIssueByIndex", err)
- }
- return
- }
-
- // Make sure type and URL matches.
- if ctx.Params(":type") == "issues" && issue.IsPull {
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
- ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
- return
- }
-
- if issue.IsPull {
- MustAllowPulls(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["PageIsPullList"] = true
- ctx.Data["PageIsPullConversation"] = true
- } else {
- MustEnableIssues(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- }
-
- if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
- ctx.Data["IssueType"] = "pulls"
- } else if !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) {
- ctx.Data["IssueType"] = "issues"
- } else {
- ctx.Data["IssueType"] = "all"
- }
-
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["RequireTribute"] = true
- ctx.Data["RequireSimpleMDE"] = true
- ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
- ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
- upload.AddUploadContext(ctx, "comment")
-
- if err = issue.LoadAttributes(); err != nil {
- ctx.ServerError("LoadAttributes", err)
- return
- }
-
- if err = filterXRefComments(ctx, issue); err != nil {
- ctx.ServerError("filterXRefComments", err)
- return
- }
-
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
-
- iw := new(models.IssueWatch)
- if ctx.User != nil {
- iw.UserID = ctx.User.ID
- iw.IssueID = issue.ID
- iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
- if err != nil {
- ctx.ServerError("CheckIssueWatch", err)
- return
- }
- }
- ctx.Data["IssueWatch"] = iw
-
- issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(),
- }, issue.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
-
- repo := ctx.Repo.Repository
-
- // Get more information if it's a pull request.
- if issue.IsPull {
- if issue.PullRequest.HasMerged {
- ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
- PrepareMergedViewPullInfo(ctx, issue)
- } else {
- PrepareViewPullInfo(ctx, issue)
- ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
- }
- if ctx.Written() {
- return
- }
- }
-
- // Metas.
- // Check labels.
- labelIDMark := make(map[int64]bool)
- for i := range issue.Labels {
- labelIDMark[issue.Labels[i].ID] = true
- }
- labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByRepoID", err)
- return
- }
- ctx.Data["Labels"] = labels
-
- if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByOrgID", err)
- return
- }
- ctx.Data["OrgLabels"] = orgLabels
-
- labels = append(labels, orgLabels...)
- }
-
- hasSelected := false
- for i := range labels {
- if labelIDMark[labels[i].ID] {
- labels[i].IsChecked = true
- hasSelected = true
- }
- }
- ctx.Data["HasSelectedLabel"] = hasSelected
-
- // Check milestone and assignee.
- if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
- RetrieveRepoMilestonesAndAssignees(ctx, repo)
- retrieveProjects(ctx, repo)
-
- if ctx.Written() {
- return
- }
- }
-
- if issue.IsPull {
- canChooseReviewer := ctx.Repo.CanWrite(models.UnitTypePullRequests)
- if !canChooseReviewer && ctx.User != nil && ctx.IsSigned {
- canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.User)
- if err != nil {
- ctx.ServerError("IsOfficialReviewer", err)
- return
- }
- }
-
- RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
- if ctx.Written() {
- return
- }
- }
-
- if ctx.IsSigned {
- // Update issue-user.
- if err = issue.ReadBy(ctx.User.ID); err != nil {
- ctx.ServerError("ReadBy", err)
- return
- }
- }
-
- var (
- tag models.CommentTag
- ok bool
- marked = make(map[int64]models.CommentTag)
- comment *models.Comment
- participants = make([]*models.User, 1, 10)
- )
- if ctx.Repo.Repository.IsTimetrackerEnabled() {
- if ctx.IsSigned {
- // Deal with the stopwatch
- ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
- if !ctx.Data["IsStopwatchRunning"].(bool) {
- var exists bool
- var sw *models.Stopwatch
- if exists, sw, err = models.HasUserStopwatch(ctx.User.ID); err != nil {
- ctx.ServerError("HasUserStopwatch", err)
- return
- }
- ctx.Data["HasUserStopwatch"] = exists
- if exists {
- // Add warning if the user has already a stopwatch
- var otherIssue *models.Issue
- if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
- ctx.ServerError("GetIssueByID", err)
- return
- }
- if err = otherIssue.LoadRepo(); err != nil {
- ctx.ServerError("LoadRepo", err)
- return
- }
- // Add link to the issue of the already running stopwatch
- ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
- }
- }
- ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
- } else {
- ctx.Data["CanUseTimetracker"] = false
- }
- if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
- ctx.ServerError("TotalTimes", err)
- return
- }
- }
-
- // Check if the user can use the dependencies
- ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull)
-
- // check if dependencies can be created across repositories
- ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
-
- if issue.ShowTag, err = commentTag(repo, issue.Poster, issue); err != nil {
- ctx.ServerError("commentTag", err)
- return
- }
- marked[issue.PosterID] = issue.ShowTag
-
- // Render comments and and fetch participants.
- participants[0] = issue.Poster
- for _, comment = range issue.Comments {
- comment.Issue = issue
-
- if err := comment.LoadPoster(); err != nil {
- ctx.ServerError("LoadPoster", err)
- return
- }
-
- if comment.Type == models.CommentTypeComment {
- if err := comment.LoadAttachments(); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
-
- comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(),
- }, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- // Check tag.
- tag, ok = marked[comment.PosterID]
- if ok {
- comment.ShowTag = tag
- continue
- }
-
- comment.ShowTag, err = commentTag(repo, comment.Poster, issue)
- if err != nil {
- ctx.ServerError("commentTag", err)
- return
- }
- marked[comment.PosterID] = comment.ShowTag
- participants = addParticipant(comment.Poster, participants)
- } else if comment.Type == models.CommentTypeLabel {
- if err = comment.LoadLabel(); err != nil {
- ctx.ServerError("LoadLabel", err)
- return
- }
- } else if comment.Type == models.CommentTypeMilestone {
- if err = comment.LoadMilestone(); err != nil {
- ctx.ServerError("LoadMilestone", err)
- return
- }
- ghostMilestone := &models.Milestone{
- ID: -1,
- Name: ctx.Tr("repo.issues.deleted_milestone"),
- }
- if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
- comment.OldMilestone = ghostMilestone
- }
- if comment.MilestoneID > 0 && comment.Milestone == nil {
- comment.Milestone = ghostMilestone
- }
- } else if comment.Type == models.CommentTypeProject {
-
- if err = comment.LoadProject(); err != nil {
- ctx.ServerError("LoadProject", err)
- return
- }
-
- ghostProject := &models.Project{
- ID: -1,
- Title: ctx.Tr("repo.issues.deleted_project"),
- }
-
- if comment.OldProjectID > 0 && comment.OldProject == nil {
- comment.OldProject = ghostProject
- }
-
- if comment.ProjectID > 0 && comment.Project == nil {
- comment.Project = ghostProject
- }
-
- } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
- if err = comment.LoadAssigneeUserAndTeam(); err != nil {
- ctx.ServerError("LoadAssigneeUserAndTeam", err)
- return
- }
- } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
- if err = comment.LoadDepIssueDetails(); err != nil {
- if !models.IsErrIssueNotExist(err) {
- ctx.ServerError("LoadDepIssueDetails", err)
- return
- }
- }
- } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
- comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Repo.RepoLink,
- Metas: ctx.Repo.Repository.ComposeMetas(),
- }, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
- ctx.ServerError("LoadReview", err)
- return
- }
- participants = addParticipant(comment.Poster, participants)
- if comment.Review == nil {
- continue
- }
- if err = comment.Review.LoadAttributes(); err != nil {
- if !models.IsErrUserNotExist(err) {
- ctx.ServerError("Review.LoadAttributes", err)
- return
- }
- comment.Review.Reviewer = models.NewGhostUser()
- }
- if err = comment.Review.LoadCodeComments(); err != nil {
- ctx.ServerError("Review.LoadCodeComments", err)
- return
- }
- for _, codeComments := range comment.Review.CodeComments {
- for _, lineComments := range codeComments {
- for _, c := range lineComments {
- // Check tag.
- tag, ok = marked[c.PosterID]
- if ok {
- c.ShowTag = tag
- continue
- }
-
- c.ShowTag, err = commentTag(repo, c.Poster, issue)
- if err != nil {
- ctx.ServerError("commentTag", err)
- return
- }
- marked[c.PosterID] = c.ShowTag
- participants = addParticipant(c.Poster, participants)
- }
- }
- }
- if err = comment.LoadResolveDoer(); err != nil {
- ctx.ServerError("LoadResolveDoer", err)
- return
- }
- } else if comment.Type == models.CommentTypePullPush {
- participants = addParticipant(comment.Poster, participants)
- if err = comment.LoadPushCommits(); err != nil {
- ctx.ServerError("LoadPushCommits", err)
- return
- }
- } else if comment.Type == models.CommentTypeAddTimeManual ||
- comment.Type == models.CommentTypeStopTracking {
- // drop error since times could be pruned from DB..
- _ = comment.LoadTime()
- }
- }
-
- // Combine multiple label assignments into a single comment
- combineLabelComments(issue)
-
- getBranchData(ctx, issue)
- if issue.IsPull {
- pull := issue.PullRequest
- pull.Issue = issue
- canDelete := false
- ctx.Data["AllowMerge"] = false
-
- if ctx.IsSigned {
- if err := pull.LoadHeadRepo(); err != nil {
- log.Error("LoadHeadRepo: %v", err)
- } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
- perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
- if perm.CanWrite(models.UnitTypeCode) {
- // Check if branch is not protected
- if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
- log.Error("IsProtectedBranch: %v", err)
- } else if !protected {
- canDelete = true
- ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index) + "/cleanup"
- }
- }
- }
-
- if err := pull.LoadBaseRepo(); err != nil {
- log.Error("LoadBaseRepo: %v", err)
- }
- perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
- ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
- if err != nil {
- ctx.ServerError("IsUserAllowedToMerge", err)
- return
- }
-
- if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
- ctx.ServerError("CanMarkConversation", err)
- return
- }
- }
-
- prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
- if err != nil {
- ctx.ServerError("GetUnit", err)
- return
- }
- prConfig := prUnit.PullRequestsConfig()
-
- // Check correct values and select default
- if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
- !prConfig.IsMergeStyleAllowed(ms) {
- defaultMergeStyle := prConfig.GetDefaultMergeStyle()
- if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
- ctx.Data["MergeStyle"] = defaultMergeStyle
- } else if prConfig.AllowMerge {
- ctx.Data["MergeStyle"] = models.MergeStyleMerge
- } else if prConfig.AllowRebase {
- ctx.Data["MergeStyle"] = models.MergeStyleRebase
- } else if prConfig.AllowRebaseMerge {
- ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
- } else if prConfig.AllowSquash {
- ctx.Data["MergeStyle"] = models.MergeStyleSquash
- } else if prConfig.AllowManualMerge {
- ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
- } else {
- ctx.Data["MergeStyle"] = ""
- }
- }
- if err = pull.LoadProtectedBranch(); err != nil {
- ctx.ServerError("LoadProtectedBranch", err)
- return
- }
- if pull.ProtectedBranch != nil {
- cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
- ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
- ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
- ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
- ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
- ctx.Data["GrantedApprovals"] = cnt
- ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
- ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
- ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
- ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
- }
- ctx.Data["WillSign"] = false
- if ctx.User != nil {
- sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
- ctx.Data["WillSign"] = sign
- ctx.Data["SigningKey"] = key
- if err != nil {
- if models.IsErrWontSign(err) {
- ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason
- } else {
- ctx.Data["WontSignReason"] = "error"
- log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
- }
- }
- } else {
- ctx.Data["WontSignReason"] = "not_signed_in"
- }
- ctx.Data["IsPullBranchDeletable"] = canDelete &&
- pull.HeadRepo != nil &&
- git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
- (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
-
- stillCanManualMerge := func() bool {
- if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
- return false
- }
- if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
- return false
- }
- if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
- return true
- }
-
- return false
- }
-
- ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
- }
-
- // Get Dependencies
- ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
- if err != nil {
- ctx.ServerError("BlockedByDependencies", err)
- return
- }
- ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
- if err != nil {
- ctx.ServerError("BlockingDependencies", err)
- return
- }
-
- ctx.Data["Participants"] = participants
- ctx.Data["NumParticipants"] = len(participants)
- ctx.Data["Issue"] = issue
- ctx.Data["ReadOnly"] = false
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
- ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
- ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
- ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeProjects)
- ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
- ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
- ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
- ctx.HTML(http.StatusOK, tplIssueView)
-}
-
-// GetActionIssue will return the issue which is used in the context.
-func GetActionIssue(ctx *context.Context) *models.Issue {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
- if err != nil {
- ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
- return nil
- }
- issue.Repo = ctx.Repo.Repository
- checkIssueRights(ctx, issue)
- if ctx.Written() {
- return nil
- }
- if err = issue.LoadAttributes(); err != nil {
- ctx.ServerError("LoadAttributes", nil)
- return nil
- }
- return issue
-}
-
-func checkIssueRights(ctx *context.Context, issue *models.Issue) {
- if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) ||
- !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
- ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
- }
-}
-
-func getActionIssues(ctx *context.Context) []*models.Issue {
- commaSeparatedIssueIDs := ctx.Query("issue_ids")
- if len(commaSeparatedIssueIDs) == 0 {
- return nil
- }
- issueIDs := make([]int64, 0, 10)
- for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
- issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
- if err != nil {
- ctx.ServerError("ParseInt", err)
- return nil
- }
- issueIDs = append(issueIDs, issueID)
- }
- issues, err := models.GetIssuesByIDs(issueIDs)
- if err != nil {
- ctx.ServerError("GetIssuesByIDs", err)
- return nil
- }
- // Check access rights for all issues
- issueUnitEnabled := ctx.Repo.CanRead(models.UnitTypeIssues)
- prUnitEnabled := ctx.Repo.CanRead(models.UnitTypePullRequests)
- for _, issue := range issues {
- if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
- ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
- return nil
- }
- if err = issue.LoadAttributes(); err != nil {
- ctx.ServerError("LoadAttributes", err)
- return nil
- }
- }
- return issues
-}
-
-// UpdateIssueTitle change issue's title
-func UpdateIssueTitle(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- if ctx.Written() {
- return
- }
-
- if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- title := ctx.QueryTrim("title")
- if len(title) == 0 {
- ctx.Error(http.StatusNoContent)
- return
- }
-
- if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil {
- ctx.ServerError("ChangeTitle", err)
- return
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "title": issue.Title,
- })
-}
-
-// UpdateIssueRef change issue's ref (branch)
-func UpdateIssueRef(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- if ctx.Written() {
- return
- }
-
- if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- ref := ctx.QueryTrim("ref")
-
- if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil {
- ctx.ServerError("ChangeRef", err)
- return
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "ref": ref,
- })
-}
-
-// UpdateIssueContent change issue's content
-func UpdateIssueContent(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- if ctx.Written() {
- return
- }
-
- if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- content := ctx.Query("content")
- if err := issue_service.ChangeContent(issue, ctx.User, content); err != nil {
- ctx.ServerError("ChangeContent", err)
- return
- }
-
- files := ctx.QueryStrings("files[]")
- if err := updateAttachments(issue, files); err != nil {
- ctx.ServerError("UpdateAttachments", err)
- return
- }
-
- content, err := markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Query("context"),
- Metas: ctx.Repo.Repository.ComposeMetas(),
- }, issue.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "content": content,
- "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
- })
-}
-
-// UpdateIssueMilestone change issue's milestone
-func UpdateIssueMilestone(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
-
- milestoneID := ctx.QueryInt64("id")
- for _, issue := range issues {
- oldMilestoneID := issue.MilestoneID
- if oldMilestoneID == milestoneID {
- continue
- }
- issue.MilestoneID = milestoneID
- if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
- ctx.ServerError("ChangeMilestoneAssign", err)
- return
- }
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "ok": true,
- })
-}
-
-// UpdateIssueAssignee change issue's or pull's assignee
-func UpdateIssueAssignee(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
-
- assigneeID := ctx.QueryInt64("id")
- action := ctx.Query("action")
-
- for _, issue := range issues {
- switch action {
- case "clear":
- if err := issue_service.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
- ctx.ServerError("ClearAssignees", err)
- return
- }
- default:
- assignee, err := models.GetUserByID(assigneeID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return
- }
-
- valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
- if err != nil {
- ctx.ServerError("canBeAssigned", err)
- return
- }
- if !valid {
- ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
- return
- }
-
- _, _, err = issue_service.ToggleAssignee(issue, ctx.User, assigneeID)
- if err != nil {
- ctx.ServerError("ToggleAssignee", err)
- return
- }
- }
- }
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "ok": true,
- })
-}
-
-// UpdatePullReviewRequest add or remove review request
-func UpdatePullReviewRequest(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
-
- reviewID := ctx.QueryInt64("id")
- action := ctx.Query("action")
-
- // TODO: Not support 'clear' now
- if action != "attach" && action != "detach" {
- ctx.Status(403)
- return
- }
-
- for _, issue := range issues {
- if err := issue.LoadRepo(); err != nil {
- ctx.ServerError("issue.LoadRepo", err)
- return
- }
-
- if !issue.IsPull {
- log.Warn(
- "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
- issue.Repo, issue.Index,
- )
- ctx.Status(403)
- return
- }
- if reviewID < 0 {
- // negative reviewIDs represent team requests
- if err := issue.Repo.GetOwner(); err != nil {
- ctx.ServerError("issue.Repo.GetOwner", err)
- return
- }
-
- if !issue.Repo.Owner.IsOrganization() {
- log.Warn(
- "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
- issue.Repo.FullName(), issue.Index, issue.Repo.ID,
- )
- ctx.Status(403)
- return
- }
-
- team, err := models.GetTeamByID(-reviewID)
- if err != nil {
- ctx.ServerError("models.GetTeamByID", err)
- return
- }
-
- if team.OrgID != issue.Repo.OwnerID {
- log.Warn(
- "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
- team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
- ctx.Status(403)
- return
- }
-
- err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue)
- if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
- log.Warn(
- "UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
- team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
- err,
- )
- ctx.Status(403)
- return
- }
- ctx.ServerError("IsValidTeamReviewRequest", err)
- return
- }
-
- _, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach")
- if err != nil {
- ctx.ServerError("TeamReviewRequest", err)
- return
- }
- continue
- }
-
- reviewer, err := models.GetUserByID(reviewID)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- log.Warn(
- "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
- reviewID, issue.Repo, issue.Index,
- err,
- )
- ctx.Status(403)
- return
- }
- ctx.ServerError("GetUserByID", err)
- return
- }
-
- err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil)
- if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
- log.Warn(
- "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
- reviewer, issue.Repo, issue.Index,
- err,
- )
- ctx.Status(403)
- return
- }
- ctx.ServerError("isValidReviewRequest", err)
- return
- }
-
- _, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach")
- if err != nil {
- ctx.ServerError("ReviewRequest", err)
- return
- }
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "ok": true,
- })
-}
-
-// UpdateIssueStatus change issue's status
-func UpdateIssueStatus(ctx *context.Context) {
- issues := getActionIssues(ctx)
- if ctx.Written() {
- return
- }
-
- var isClosed bool
- switch action := ctx.Query("action"); action {
- case "open":
- isClosed = false
- case "close":
- isClosed = true
- default:
- log.Warn("Unrecognized action: %s", action)
- }
-
- if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
- ctx.ServerError("LoadRepositories", err)
- return
- }
- for _, issue := range issues {
- if issue.IsClosed != isClosed {
- if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
- if models.IsErrDependenciesLeft(err) {
- ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
- "error": "cannot close this issue because it still has open dependencies",
- })
- return
- }
- ctx.ServerError("ChangeStatus", err)
- return
- }
- }
- }
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "ok": true,
- })
-}
-
-// NewComment create a comment for issue
-func NewComment(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateCommentForm)
- 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
- }
-
- if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
- ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
- ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
- return
- }
-
- var attachments []string
- if setting.Attachment.Enabled {
- attachments = form.Files
- }
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(issue.HTMLURL())
- return
- }
-
- var comment *models.Comment
- defer func() {
- // Check if issue admin/poster changes the status of issue.
- if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
- (form.Status == "reopen" || form.Status == "close") &&
- !(issue.IsPull && issue.PullRequest.HasMerged) {
-
- // Duplication and conflict check should apply to reopen pull request.
- var pr *models.PullRequest
-
- if form.Status == "reopen" && issue.IsPull {
- pull := issue.PullRequest
- var err error
- pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
- if err != nil {
- if !models.IsErrPullRequestNotExist(err) {
- ctx.ServerError("GetUnmergedPullRequest", err)
- return
- }
- }
-
- // Regenerate patch and test conflict.
- if pr == nil {
- pull_service.AddToTaskQueue(issue.PullRequest)
- }
- }
-
- if pr != nil {
- ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
- } else {
- isClosed := form.Status == "close"
- if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
- log.Error("ChangeStatus: %v", err)
-
- if models.IsErrDependenciesLeft(err) {
- if issue.IsPull {
- ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
- ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
- } else {
- ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
- ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
- }
- return
- }
- } else {
- if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
- ctx.ServerError("CreateOrStopIssueStopwatch", err)
- return
- }
-
- log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
- }
- }
- }
-
- // Redirect to comment hashtag if there is any actual content.
- typeName := "issues"
- if issue.IsPull {
- typeName = "pulls"
- }
- if comment != nil {
- ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
- } else {
- ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
- }
- }()
-
- // Fix #321: Allow empty comments, as long as we have attachments.
- if len(form.Content) == 0 && len(attachments) == 0 {
- return
- }
-
- comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
- if err != nil {
- ctx.ServerError("CreateIssueComment", err)
- return
- }
-
- log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
-}
-
-// UpdateCommentContent change comment of issue's content
-func UpdateCommentContent(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
- return
- }
-
- if comment.Type == models.CommentTypeComment {
- if err := comment.LoadAttachments(); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
- }
-
- if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
- return
- } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
- ctx.Error(http.StatusNoContent)
- return
- }
-
- oldContent := comment.Content
- comment.Content = ctx.Query("content")
- if len(comment.Content) == 0 {
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "content": "",
- })
- return
- }
- if err = comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
- ctx.ServerError("UpdateComment", err)
- return
- }
-
- files := ctx.QueryStrings("files[]")
- if err := updateAttachments(comment, files); err != nil {
- ctx.ServerError("UpdateAttachments", err)
- return
- }
-
- content, err := markdown.RenderString(&markup.RenderContext{
- URLPrefix: ctx.Query("context"),
- Metas: ctx.Repo.Repository.ComposeMetas(),
- }, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "content": content,
- "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
- })
-}
-
-// DeleteComment delete comment of issue
-func DeleteComment(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
- return
- }
-
- if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
- return
- } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
- ctx.Error(http.StatusNoContent)
- return
- }
-
- if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
- ctx.ServerError("DeleteCommentByID", err)
- return
- }
-
- ctx.Status(200)
-}
-
-// ChangeIssueReaction create a reaction for issue
-func ChangeIssueReaction(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.ReactionForm)
- 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
- }
-
- if ctx.HasError() {
- ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
- return
- }
-
- switch ctx.Params(":action") {
- case "react":
- reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
- if err != nil {
- if models.IsErrForbiddenIssueReaction(err) {
- ctx.ServerError("ChangeIssueReaction", err)
- return
- }
- log.Info("CreateIssueReaction: %s", err)
- break
- }
- // Reload new reactions
- issue.Reactions = nil
- if err = issue.LoadAttributes(); err != nil {
- log.Info("issue.LoadAttributes: %s", err)
- break
- }
-
- log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
- case "unreact":
- if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
- ctx.ServerError("DeleteIssueReaction", err)
- return
- }
-
- // Reload new reactions
- issue.Reactions = nil
- if err := issue.LoadAttributes(); err != nil {
- log.Info("issue.LoadAttributes: %s", err)
- break
- }
-
- log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
- default:
- ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
- return
- }
-
- if len(issue.Reactions) == 0 {
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "empty": true,
- "html": "",
- })
- return
- }
-
- html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
- "ctx": ctx.Data,
- "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
- "Reactions": issue.Reactions.GroupByType(),
- })
- if err != nil {
- ctx.ServerError("ChangeIssueReaction.HTMLString", err)
- return
- }
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "html": html,
- })
-}
-
-// ChangeCommentReaction create a reaction for comment
-func ChangeCommentReaction(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.ReactionForm)
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
- return
- }
-
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
- return
- }
-
- if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
- if log.IsTrace() {
- if ctx.IsSigned {
- issueType := "issues"
- if comment.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(comment.Issue.PosterID),
- issueType,
- ctx.Repo.Repository,
- ctx.Repo.Permission)
- } else {
- log.Trace("Permission Denied: Not logged in")
- }
- }
-
- ctx.Error(http.StatusForbidden)
- return
- } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
- ctx.Error(http.StatusNoContent)
- return
- }
-
- switch ctx.Params(":action") {
- case "react":
- reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
- if err != nil {
- if models.IsErrForbiddenIssueReaction(err) {
- ctx.ServerError("ChangeIssueReaction", err)
- return
- }
- log.Info("CreateCommentReaction: %s", err)
- break
- }
- // Reload new reactions
- comment.Reactions = nil
- if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
- log.Info("comment.LoadReactions: %s", err)
- break
- }
-
- log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
- case "unreact":
- if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
- ctx.ServerError("DeleteCommentReaction", err)
- return
- }
-
- // Reload new reactions
- comment.Reactions = nil
- if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
- log.Info("comment.LoadReactions: %s", err)
- break
- }
-
- log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
- default:
- ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
- return
- }
-
- if len(comment.Reactions) == 0 {
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "empty": true,
- "html": "",
- })
- return
- }
-
- html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
- "ctx": ctx.Data,
- "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
- "Reactions": comment.Reactions.GroupByType(),
- })
- if err != nil {
- ctx.ServerError("ChangeCommentReaction.HTMLString", err)
- return
- }
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "html": html,
- })
-}
-
-func addParticipant(poster *models.User, participants []*models.User) []*models.User {
- for _, part := range participants {
- if poster.ID == part.ID {
- return participants
- }
- }
- return append(participants, poster)
-}
-
-func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
- // Remove comments that the user has no permissions to see
- for i := 0; i < len(issue.Comments); {
- c := issue.Comments[i]
- if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
- var err error
- // Set RefRepo for description in template
- c.RefRepo, err = models.GetRepositoryByID(c.RefRepoID)
- if err != nil {
- return err
- }
- perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.User)
- if err != nil {
- return err
- }
- if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
- issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
- continue
- }
- }
- i++
- }
- return nil
-}
-
-// GetIssueAttachments returns attachments for the issue
-func GetIssueAttachments(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- var attachments = make([]*api.Attachment, len(issue.Attachments))
- for i := 0; i < len(issue.Attachments); i++ {
- attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
- }
- ctx.JSON(http.StatusOK, attachments)
-}
-
-// GetCommentAttachments returns attachments for the comment
-func GetCommentAttachments(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
- if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
- return
- }
- var attachments = make([]*api.Attachment, 0)
- if comment.Type == models.CommentTypeComment {
- if err := comment.LoadAttachments(); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
- for i := 0; i < len(comment.Attachments); i++ {
- attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
- }
- }
- ctx.JSON(http.StatusOK, attachments)
-}
-
-func updateAttachments(item interface{}, files []string) error {
- var attachments []*models.Attachment
- switch content := item.(type) {
- case *models.Issue:
- attachments = content.Attachments
- case *models.Comment:
- attachments = content.Attachments
- default:
- return fmt.Errorf("Unknown Type: %T", content)
- }
- for i := 0; i < len(attachments); i++ {
- if util.IsStringInSlice(attachments[i].UUID, files) {
- continue
- }
- if err := models.DeleteAttachment(attachments[i], true); err != nil {
- return err
- }
- }
- var err error
- if len(files) > 0 {
- switch content := item.(type) {
- case *models.Issue:
- err = content.UpdateAttachments(files)
- case *models.Comment:
- err = content.UpdateAttachments(files)
- default:
- return fmt.Errorf("Unknown Type: %T", content)
- }
- if err != nil {
- return err
- }
- }
- switch content := item.(type) {
- case *models.Issue:
- content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
- case *models.Comment:
- content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
- default:
- return fmt.Errorf("Unknown Type: %T", content)
- }
- return err
-}
-
-func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment, content string) string {
- attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
- "ctx": ctx.Data,
- "Attachments": attachments,
- "Content": content,
- })
- if err != nil {
- ctx.ServerError("attachmentsHTML.HTMLString", err)
- return ""
- }
- return attachHTML
-}
-
-// combineLabelComments combine the nearby label comments as one.
-func combineLabelComments(issue *models.Issue) {
- var prev, cur *models.Comment
- for i := 0; i < len(issue.Comments); i++ {
- cur = issue.Comments[i]
- if i > 0 {
- prev = issue.Comments[i-1]
- }
- if i == 0 || cur.Type != models.CommentTypeLabel ||
- (prev != nil && prev.PosterID != cur.PosterID) ||
- (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
- if cur.Type == models.CommentTypeLabel && cur.Label != nil {
- if cur.Content != "1" {
- cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
- } else {
- cur.AddedLabels = append(cur.AddedLabels, cur.Label)
- }
- }
- continue
- }
-
- if cur.Label != nil { // now cur MUST be label comment
- if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment
- if cur.Content != "1" {
- prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
- } else {
- prev.AddedLabels = append(prev.AddedLabels, cur.Label)
- }
- prev.CreatedUnix = cur.CreatedUnix
- // remove the current comment since it has been combined to prev comment
- issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
- i--
- } else { // if prev is not a label comment, start a new group
- if cur.Content != "1" {
- cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
- } else {
- cur.AddedLabels = append(cur.AddedLabels, cur.Label)
- }
- }
- }
- }
-}
-
-// get all teams that current user can mention
-func handleTeamMentions(ctx *context.Context) {
- if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
- return
- }
-
- isAdmin := false
- var err error
- // Admin has super access.
- if ctx.User.IsAdmin {
- isAdmin = true
- } else {
- isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
- if err != nil {
- ctx.ServerError("IsOwnedBy", err)
- return
- }
- }
-
- if isAdmin {
- if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
- ctx.ServerError("GetTeams", err)
- return
- }
- } else {
- ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
- if err != nil {
- ctx.ServerError("GetUserTeams", err)
- return
- }
- }
-
- ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
- ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
- ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.RelAvatarLink()
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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,
- })
-}
+++ /dev/null
-// 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{})
- }
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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)
- })
- }
-}
+++ /dev/null
-// 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())
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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("..", ".."))
-}
+++ /dev/null
-// 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"] = ""
- }
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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 + "/")
-}
+++ /dev/null
-// 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())
-}
+++ /dev/null
-// Copyright 2018 The Gitea Authors.
-// 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 (
- "container/list"
- "crypto/subtle"
- "errors"
- "fmt"
- "net/http"
- "path"
- "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/notification"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/upload"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/utils"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/gitdiff"
- pull_service "code.gitea.io/gitea/services/pull"
- repo_service "code.gitea.io/gitea/services/repository"
- "github.com/unknwon/com"
-)
-
-const (
- tplFork base.TplName = "repo/pulls/fork"
- tplCompareDiff base.TplName = "repo/diff/compare"
- tplPullCommits base.TplName = "repo/pulls/commits"
- tplPullFiles base.TplName = "repo/pulls/files"
-
- pullRequestTemplateKey = "PullRequestTemplate"
-)
-
-var (
- pullRequestTemplateCandidates = []string{
- "PULL_REQUEST_TEMPLATE.md",
- "pull_request_template.md",
- ".gitea/PULL_REQUEST_TEMPLATE.md",
- ".gitea/pull_request_template.md",
- ".github/PULL_REQUEST_TEMPLATE.md",
- ".github/pull_request_template.md",
- }
-)
-
-func getRepository(ctx *context.Context, repoID int64) *models.Repository {
- repo, err := models.GetRepositoryByID(repoID)
- if err != nil {
- if models.IsErrRepoNotExist(err) {
- ctx.NotFound("GetRepositoryByID", nil)
- } else {
- ctx.ServerError("GetRepositoryByID", err)
- }
- return nil
- }
-
- perm, err := models.GetUserRepoPermission(repo, ctx.User)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return nil
- }
-
- if !perm.CanRead(models.UnitTypeCode) {
- log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
- "User in repo has Permissions: %-+v",
- ctx.User,
- models.UnitTypeCode,
- ctx.Repo,
- perm)
- ctx.NotFound("getRepository", nil)
- return nil
- }
- return repo
-}
-
-func getForkRepository(ctx *context.Context) *models.Repository {
- forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
- if ctx.Written() {
- return nil
- }
-
- if forkRepo.IsEmpty {
- log.Trace("Empty repository %-v", forkRepo)
- ctx.NotFound("getForkRepository", nil)
- return nil
- }
-
- if err := forkRepo.GetOwner(); err != nil {
- ctx.ServerError("GetOwner", err)
- return nil
- }
-
- ctx.Data["repo_name"] = forkRepo.Name
- ctx.Data["description"] = forkRepo.Description
- ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
- canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID)
-
- ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name
- ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID
-
- if err := ctx.User.GetOwnedOrganizations(); err != nil {
- ctx.ServerError("GetOwnedOrganizations", err)
- return nil
- }
- var orgs []*models.User
- for _, org := range ctx.User.OwnedOrgs {
- if forkRepo.OwnerID != org.ID && !org.HasForkedRepo(forkRepo.ID) {
- orgs = append(orgs, org)
- }
- }
-
- var traverseParentRepo = forkRepo
- var err error
- for {
- if ctx.User.ID == traverseParentRepo.OwnerID {
- canForkToUser = false
- } else {
- for i, org := range orgs {
- if org.ID == traverseParentRepo.OwnerID {
- orgs = append(orgs[:i], orgs[i+1:]...)
- break
- }
- }
- }
-
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return nil
- }
- }
-
- ctx.Data["CanForkToUser"] = canForkToUser
- ctx.Data["Orgs"] = orgs
-
- if canForkToUser {
- ctx.Data["ContextUser"] = ctx.User
- } else if len(orgs) > 0 {
- ctx.Data["ContextUser"] = orgs[0]
- }
-
- return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("new_fork")
-
- getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateRepoForm)
- ctx.Data["Title"] = ctx.Tr("new_fork")
-
- ctxUser := checkContextUser(ctx, form.UID)
- if ctx.Written() {
- return
- }
-
- forkRepo := getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.Data["ContextUser"] = ctxUser
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
- return
- }
-
- var err error
- var traverseParentRepo = forkRepo
- for {
- if ctxUser.ID == traverseParentRepo.OwnerID {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- return
- }
- repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID)
- if has {
- ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
- return
- }
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return
- }
- }
-
- // Check ownership of organization.
- if ctxUser.IsOrganization() {
- isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
- if err != nil {
- ctx.ServerError("IsOwnedBy", err)
- return
- } else if !isOwner {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description)
- if err != nil {
- ctx.Data["Err_RepoName"] = true
- switch {
- case models.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- case models.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplFork, &form)
- case models.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
- default:
- ctx.ServerError("ForkPost", err)
- }
- return
- }
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
-}
-
-func checkPullInfo(ctx *context.Context) *models.Issue {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
- if err != nil {
- if models.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
- } else {
- ctx.ServerError("GetIssueByIndex", err)
- }
- return nil
- }
- if err = issue.LoadPoster(); err != nil {
- ctx.ServerError("LoadPoster", err)
- return nil
- }
- if err := issue.LoadRepo(); err != nil {
- ctx.ServerError("LoadRepo", err)
- return nil
- }
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
- ctx.Data["Issue"] = issue
-
- if !issue.IsPull {
- ctx.NotFound("ViewPullCommits", nil)
- return nil
- }
-
- if err = issue.LoadPullRequest(); err != nil {
- ctx.ServerError("LoadPullRequest", err)
- return nil
- }
-
- if err = issue.PullRequest.LoadHeadRepo(); err != nil {
- ctx.ServerError("LoadHeadRepo", err)
- return nil
- }
-
- if ctx.IsSigned {
- // Update issue-user.
- if err = issue.ReadBy(ctx.User.ID); err != nil {
- ctx.ServerError("ReadBy", err)
- return nil
- }
- }
-
- return issue
-}
-
-func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
- if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
- ctx.Data["HeadTarget"] = pull.HeadBranch
- } else if pull.HeadRepo == nil {
- ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
- } else {
- ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
- }
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
- ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL()
-}
-
-// PrepareMergedViewPullInfo show meta information for a merged pull request view page
-func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
- pull := issue.PullRequest
-
- setMergeTarget(ctx, pull)
- ctx.Data["HasMerged"] = true
-
- compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
- pull.MergeBase, pull.GetGitRefName())
- if err != nil {
- if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
- ctx.Data["IsPullRequestBroken"] = true
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["NumCommits"] = 0
- ctx.Data["NumFiles"] = 0
- return nil
- }
-
- ctx.ServerError("GetCompareInfo", err)
- return nil
- }
- ctx.Data["NumCommits"] = compareInfo.Commits.Len()
- ctx.Data["NumFiles"] = compareInfo.NumFiles
-
- if compareInfo.Commits.Len() != 0 {
- sha := compareInfo.Commits.Front().Value.(*git.Commit).ID.String()
- commitStatuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, sha, models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLatestCommitStatus", err)
- return nil
- }
- if len(commitStatuses) != 0 {
- ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
- }
- }
-
- return compareInfo
-}
-
-// PrepareViewPullInfo show meta information for a pull request preview page
-func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
- repo := ctx.Repo.Repository
- pull := issue.PullRequest
-
- if err := pull.LoadHeadRepo(); err != nil {
- ctx.ServerError("LoadHeadRepo", err)
- return nil
- }
-
- if err := pull.LoadBaseRepo(); err != nil {
- ctx.ServerError("LoadBaseRepo", err)
- return nil
- }
-
- setMergeTarget(ctx, pull)
-
- if err := pull.LoadProtectedBranch(); err != nil {
- ctx.ServerError("LoadProtectedBranch", err)
- return nil
- }
- ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
-
- baseGitRepo, err := git.OpenRepository(pull.BaseRepo.RepoPath())
- if err != nil {
- ctx.ServerError("OpenRepository", err)
- return nil
- }
- defer baseGitRepo.Close()
-
- if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
- ctx.Data["IsPullRequestBroken"] = true
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["HeadTarget"] = pull.HeadBranch
-
- sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
- if err != nil {
- ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
- return nil
- }
- commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLatestCommitStatus", err)
- return nil
- }
- if len(commitStatuses) > 0 {
- ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
- }
-
- compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
- pull.MergeBase, pull.GetGitRefName())
- if err != nil {
- if strings.Contains(err.Error(), "fatal: Not a valid object name") {
- ctx.Data["IsPullRequestBroken"] = true
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["NumCommits"] = 0
- ctx.Data["NumFiles"] = 0
- return nil
- }
-
- ctx.ServerError("GetCompareInfo", err)
- return nil
- }
-
- ctx.Data["NumCommits"] = compareInfo.Commits.Len()
- ctx.Data["NumFiles"] = compareInfo.NumFiles
- return compareInfo
- }
-
- var headBranchExist bool
- var headBranchSha string
- // HeadRepo may be missing
- if pull.HeadRepo != nil {
- headGitRepo, err := git.OpenRepository(pull.HeadRepo.RepoPath())
- if err != nil {
- ctx.ServerError("OpenRepository", err)
- return nil
- }
- defer headGitRepo.Close()
-
- headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
-
- if headBranchExist {
- headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
- if err != nil {
- ctx.ServerError("GetBranchCommitID", err)
- return nil
- }
- }
- }
-
- if headBranchExist {
- ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
- if err != nil {
- ctx.ServerError("IsUserAllowedToUpdate", err)
- return nil
- }
- ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(pull)
- }
-
- sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.Data["IsPullRequestBroken"] = true
- if pull.IsSameRepo() {
- ctx.Data["HeadTarget"] = pull.HeadBranch
- } else if pull.HeadRepo == nil {
- ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
- } else {
- ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
- }
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["NumCommits"] = 0
- ctx.Data["NumFiles"] = 0
- return nil
- }
- ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
- return nil
- }
-
- commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLatestCommitStatus", err)
- return nil
- }
- if len(commitStatuses) > 0 {
- ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
- }
-
- if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
- ctx.Data["is_context_required"] = func(context string) bool {
- for _, c := range pull.ProtectedBranch.StatusCheckContexts {
- if c == context {
- return true
- }
- }
- return false
- }
- ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
- }
-
- ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
- ctx.Data["HeadBranchCommitID"] = headBranchSha
- ctx.Data["PullHeadCommitID"] = sha
-
- if pull.HeadRepo == nil || !headBranchExist || headBranchSha != sha {
- ctx.Data["IsPullRequestBroken"] = true
- if pull.IsSameRepo() {
- ctx.Data["HeadTarget"] = pull.HeadBranch
- } else if pull.HeadRepo == nil {
- ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
- } else {
- ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
- }
- }
-
- compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
- git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName())
- if err != nil {
- if strings.Contains(err.Error(), "fatal: Not a valid object name") {
- ctx.Data["IsPullRequestBroken"] = true
- ctx.Data["BaseTarget"] = pull.BaseBranch
- ctx.Data["NumCommits"] = 0
- ctx.Data["NumFiles"] = 0
- return nil
- }
-
- ctx.ServerError("GetCompareInfo", err)
- return nil
- }
-
- ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
-
- if pull.IsWorkInProgress() {
- ctx.Data["IsPullWorkInProgress"] = true
- ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
- }
-
- if pull.IsFilesConflicted() {
- ctx.Data["IsPullFilesConflicted"] = true
- ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
- }
-
- ctx.Data["NumCommits"] = compareInfo.Commits.Len()
- ctx.Data["NumFiles"] = compareInfo.NumFiles
- return compareInfo
-}
-
-// ViewPullCommits show commits for a pull request
-func ViewPullCommits(ctx *context.Context) {
- ctx.Data["PageIsPullList"] = true
- ctx.Data["PageIsPullCommits"] = true
-
- issue := checkPullInfo(ctx)
- if ctx.Written() {
- return
- }
- pull := issue.PullRequest
-
- var commits *list.List
- var prInfo *git.CompareInfo
- if pull.HasMerged {
- prInfo = PrepareMergedViewPullInfo(ctx, issue)
- } else {
- prInfo = PrepareViewPullInfo(ctx, issue)
- }
-
- if ctx.Written() {
- return
- } else if prInfo == nil {
- ctx.NotFound("ViewPullCommits", nil)
- return
- }
-
- ctx.Data["Username"] = ctx.Repo.Owner.Name
- ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- commits = prInfo.Commits
- commits = models.ValidateCommitsWithEmails(commits)
- commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
- commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
- ctx.Data["Commits"] = commits
- ctx.Data["CommitCount"] = commits.Len()
-
- getBranchData(ctx, issue)
- ctx.HTML(http.StatusOK, tplPullCommits)
-}
-
-// ViewPullFiles render pull request changed files list page
-func ViewPullFiles(ctx *context.Context) {
- ctx.Data["PageIsPullList"] = true
- ctx.Data["PageIsPullFiles"] = true
-
- issue := checkPullInfo(ctx)
- if ctx.Written() {
- return
- }
- pull := issue.PullRequest
-
- var (
- diffRepoPath string
- startCommitID string
- endCommitID string
- gitRepo *git.Repository
- )
-
- var prInfo *git.CompareInfo
- if pull.HasMerged {
- prInfo = PrepareMergedViewPullInfo(ctx, issue)
- } else {
- prInfo = PrepareViewPullInfo(ctx, issue)
- }
-
- if ctx.Written() {
- return
- } else if prInfo == nil {
- ctx.NotFound("ViewPullFiles", nil)
- return
- }
-
- diffRepoPath = ctx.Repo.GitRepo.Path
- gitRepo = ctx.Repo.GitRepo
-
- headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
- if err != nil {
- ctx.ServerError("GetRefCommitID", err)
- return
- }
-
- startCommitID = prInfo.MergeBase
- endCommitID = headCommitID
-
- ctx.Data["Username"] = ctx.Repo.Owner.Name
- ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- ctx.Data["AfterCommitID"] = endCommitID
-
- diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(diffRepoPath,
- startCommitID, endCommitID, setting.Git.MaxGitDiffLines,
- setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
- gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
- if err != nil {
- ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
- return
- }
-
- if err = diff.LoadComments(issue, ctx.User); err != nil {
- ctx.ServerError("LoadComments", err)
- return
- }
-
- if err = pull.LoadProtectedBranch(); err != nil {
- ctx.ServerError("LoadProtectedBranch", err)
- return
- }
-
- if pull.ProtectedBranch != nil {
- glob := pull.ProtectedBranch.GetProtectedFilePatterns()
- if len(glob) != 0 {
- for _, file := range diff.Files {
- file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name)
- }
- }
- }
-
- ctx.Data["Diff"] = diff
- ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
-
- baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
- if err != nil {
- ctx.ServerError("GetCommit", err)
- return
- }
- commit, err := gitRepo.GetCommit(endCommitID)
- if err != nil {
- ctx.ServerError("GetCommit", err)
- return
- }
-
- if ctx.IsSigned && ctx.User != nil {
- if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
- ctx.ServerError("CanMarkConversation", err)
- return
- }
- }
-
- headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
- setCompareContext(ctx, baseCommit, commit, headTarget)
-
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["RequireSimpleMDE"] = true
- ctx.Data["RequireTribute"] = true
- if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {
- ctx.ServerError("GetAssignees", err)
- return
- }
- handleTeamMentions(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
- if err != nil && !models.IsErrReviewNotExist(err) {
- ctx.ServerError("GetCurrentReview", err)
- return
- }
- getBranchData(ctx, issue)
- ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
- ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
- ctx.HTML(http.StatusOK, tplPullFiles)
-}
-
-// UpdatePullRequest merge PR's baseBranch into headBranch
-func UpdatePullRequest(ctx *context.Context) {
- issue := checkPullInfo(ctx)
- if ctx.Written() {
- return
- }
- if issue.IsClosed {
- ctx.NotFound("MergePullRequest", nil)
- return
- }
- if issue.PullRequest.HasMerged {
- ctx.NotFound("MergePullRequest", nil)
- return
- }
-
- if err := issue.PullRequest.LoadBaseRepo(); err != nil {
- ctx.ServerError("LoadBaseRepo", err)
- return
- }
- if err := issue.PullRequest.LoadHeadRepo(); err != nil {
- ctx.ServerError("LoadHeadRepo", err)
- return
- }
-
- allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
- if err != nil {
- ctx.ServerError("IsUserAllowedToMerge", err)
- return
- }
-
- // ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
- if !allowedUpdate {
- ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- }
-
- // default merge commit message
- message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
-
- if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil {
- if models.IsErrMergeConflicts(err) {
- conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
- "Message": ctx.Tr("repo.pulls.merge_conflict"),
- "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
- "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
- })
- if err != nil {
- ctx.ServerError("UpdatePullRequest.HTMLString", err)
- return
- }
- ctx.Flash.Error(flashError)
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- }
- ctx.Flash.Error(err.Error())
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- }
-
- time.Sleep(1 * time.Second)
-
- ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
-}
-
-// MergePullRequest response for merging pull request
-func MergePullRequest(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.MergePullRequestForm)
- issue := checkPullInfo(ctx)
- if ctx.Written() {
- return
- }
- if issue.IsClosed {
- if issue.IsPull {
- ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- }
- ctx.Flash.Error(ctx.Tr("repo.issues.closed_title"))
- ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
- return
- }
-
- pr := issue.PullRequest
-
- allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
- if err != nil {
- ctx.ServerError("IsUserAllowedToMerge", err)
- return
- }
- if !allowedMerge {
- ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
- return
- }
-
- if pr.HasMerged {
- ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
- return
- }
-
- // handle manually-merged mark
- if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
- if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
- if models.IsErrInvalidMergeStyle(err) {
- ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
- return
- } else if strings.Contains(err.Error(), "Wrong commit ID") {
- ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
- return
- }
-
- ctx.ServerError("MergedManually", err)
- return
- }
-
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
- return
- }
-
- if !pr.CanAutoMerge() {
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
- return
- }
-
- if pr.IsWorkInProgress() {
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- }
-
- if err := pull_service.CheckPRReadyToMerge(pr, false); err != nil {
- if !models.IsErrNotAllowedToMerge(err) {
- ctx.ServerError("Merge PR status", err)
- return
- }
- if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
- ctx.ServerError("IsUserRepoAdmin", err)
- return
- } else if !isRepoAdmin {
- ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- }
- }
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- }
-
- message := strings.TrimSpace(form.MergeTitleField)
- if len(message) == 0 {
- if models.MergeStyle(form.Do) == models.MergeStyleMerge {
- message = pr.GetDefaultMergeMessage()
- }
- if models.MergeStyle(form.Do) == models.MergeStyleRebaseMerge {
- message = pr.GetDefaultMergeMessage()
- }
- if models.MergeStyle(form.Do) == models.MergeStyleSquash {
- message = pr.GetDefaultSquashMessage()
- }
- }
-
- form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
- if len(form.MergeMessageField) > 0 {
- message += "\n\n" + form.MergeMessageField
- }
-
- pr.Issue = issue
- pr.Issue.Repo = ctx.Repo.Repository
-
- noDeps, err := models.IssueNoDependenciesLeft(issue)
- if err != nil {
- return
- }
-
- if !noDeps {
- ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- }
-
- if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
- if models.IsErrInvalidMergeStyle(err) {
- ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- } else if models.IsErrMergeConflicts(err) {
- conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
- "Message": ctx.Tr("repo.editor.merge_conflict"),
- "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
- "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
- })
- if err != nil {
- ctx.ServerError("MergePullRequest.HTMLString", err)
- return
- }
- ctx.Flash.Error(flashError)
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- } else if models.IsErrRebaseConflicts(err) {
- conflictError := err.(models.ErrRebaseConflicts)
- flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
- "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
- "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
- "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
- })
- if err != nil {
- ctx.ServerError("MergePullRequest.HTMLString", err)
- return
- }
- ctx.Flash.Error(flashError)
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- } else if models.IsErrMergeUnrelatedHistories(err) {
- log.Debug("MergeUnrelatedHistories error: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- } else if git.IsErrPushOutOfDate(err) {
- log.Debug("MergePushOutOfDate error: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- } else if git.IsErrPushRejected(err) {
- log.Debug("MergePushRejected error: %v", err)
- pushrejErr := err.(*git.ErrPushRejected)
- message := pushrejErr.Message
- if len(message) == 0 {
- ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
- } else {
- flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
- "Message": ctx.Tr("repo.pulls.push_rejected"),
- "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
- })
- if err != nil {
- ctx.ServerError("MergePullRequest.HTMLString", err)
- return
- }
- ctx.Flash.Error(flashError)
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
- return
- }
- ctx.ServerError("Merge", err)
- return
- }
-
- if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
- ctx.ServerError("CreateOrStopIssueStopwatch", err)
- return
- }
-
- log.Trace("Pull request merged: %d", pr.ID)
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
-}
-
-func stopTimerIfAvailable(user *models.User, issue *models.Issue) error {
-
- if models.StopwatchExists(user.ID, issue.ID) {
- if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// CompareAndPullRequestPost response for creating pull request
-func CompareAndPullRequestPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateIssueForm)
- ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
- ctx.Data["PageIsComparePull"] = true
- ctx.Data["IsDiffCompare"] = true
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
- ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
- upload.AddUploadContext(ctx, "comment")
-
- var (
- repo = ctx.Repo.Repository
- attachments []string
- )
-
- headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
- if ctx.Written() {
- return
- }
- defer headGitRepo.Close()
-
- labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
- if ctx.Written() {
- return
- }
-
- if setting.Attachment.Enabled {
- attachments = form.Files
- }
-
- if ctx.HasError() {
- middleware.AssignForm(form, ctx.Data)
-
- // This stage is already stop creating new pull request, so it does not matter if it has
- // something to compare or not.
- PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
- gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplCompareDiff)
- return
- }
-
- if util.IsEmptyString(form.Title) {
- PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
- gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
- if ctx.Written() {
- return
- }
-
- ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form)
- return
- }
-
- pullIssue := &models.Issue{
- RepoID: repo.ID,
- Title: form.Title,
- PosterID: ctx.User.ID,
- Poster: ctx.User,
- MilestoneID: milestoneID,
- IsPull: true,
- Content: form.Content,
- }
- pullRequest := &models.PullRequest{
- HeadRepoID: headRepo.ID,
- BaseRepoID: repo.ID,
- HeadBranch: headBranch,
- BaseBranch: baseBranch,
- HeadRepo: headRepo,
- BaseRepo: repo,
- MergeBase: prInfo.MergeBase,
- Type: models.PullRequestGitea,
- }
- // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
- // instead of 500.
-
- if err := pull_service.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
- ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
- } else if git.IsErrPushRejected(err) {
- pushrejErr := err.(*git.ErrPushRejected)
- message := pushrejErr.Message
- if len(message) == 0 {
- ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
- } else {
- flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
- "Message": ctx.Tr("repo.pulls.push_rejected"),
- "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
- })
- if err != nil {
- ctx.ServerError("CompareAndPullRequest.HTMLString", err)
- return
- }
- ctx.Flash.Error(flashError)
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
- return
- }
- ctx.ServerError("NewPullRequest", err)
- return
- }
-
- log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
- ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
-}
-
-// TriggerTask response for a trigger task request
-func TriggerTask(ctx *context.Context) {
- pusherID := ctx.QueryInt64("pusher")
- branch := ctx.Query("branch")
- secret := ctx.Query("secret")
- if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 {
- ctx.Error(http.StatusNotFound)
- log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid")
- return
- }
- owner, repo := parseOwnerAndRepo(ctx)
- if ctx.Written() {
- return
- }
- got := []byte(base.EncodeMD5(owner.Salt))
- want := []byte(secret)
- if subtle.ConstantTimeCompare(got, want) != 1 {
- ctx.Error(http.StatusNotFound)
- log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name)
- return
- }
-
- pusher, err := models.GetUserByID(pusherID)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.Error(http.StatusNotFound)
- } else {
- ctx.ServerError("GetUserByID", err)
- }
- return
- }
-
- log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
-
- go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
- ctx.Status(202)
-}
-
-// CleanUpPullRequest responses for delete merged branch when PR has been merged
-func CleanUpPullRequest(ctx *context.Context) {
- issue := checkPullInfo(ctx)
- if ctx.Written() {
- return
- }
-
- pr := issue.PullRequest
-
- // Don't cleanup unmerged and unclosed PRs
- if !pr.HasMerged && !issue.IsClosed {
- ctx.NotFound("CleanUpPullRequest", nil)
- return
- }
-
- if err := pr.LoadHeadRepo(); err != nil {
- ctx.ServerError("LoadHeadRepo", err)
- return
- } else if pr.HeadRepo == nil {
- // Forked repository has already been deleted
- ctx.NotFound("CleanUpPullRequest", nil)
- return
- } else if err = pr.LoadBaseRepo(); err != nil {
- ctx.ServerError("LoadBaseRepo", err)
- return
- } else if err = pr.HeadRepo.GetOwner(); err != nil {
- ctx.ServerError("HeadRepo.GetOwner", err)
- return
- }
-
- perm, err := models.GetUserRepoPermission(pr.HeadRepo, ctx.User)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
- if !perm.CanWrite(models.UnitTypeCode) {
- ctx.NotFound("CleanUpPullRequest", nil)
- return
- }
-
- fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
-
- gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
- if err != nil {
- ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
- return
- }
- defer gitRepo.Close()
-
- gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
- if err != nil {
- ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
- return
- }
- defer gitBaseRepo.Close()
-
- defer func() {
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": pr.BaseRepo.Link() + "/pulls/" + fmt.Sprint(issue.Index),
- })
- }()
-
- // Check if branch has no new commits
- headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
- if err != nil {
- log.Error("GetRefCommitID: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- return
- }
- branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
- if err != nil {
- log.Error("GetBranchCommitID: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- return
- }
- if headCommitID != branchCommitID {
- ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
- return
- }
-
- if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
- switch {
- case git.IsErrBranchNotExist(err):
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- case errors.Is(err, repo_service.ErrBranchIsDefault):
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- case errors.Is(err, repo_service.ErrBranchIsProtected):
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- default:
- log.Error("DeleteBranch: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- }
- return
- }
-
- if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, issue.ID, pr.HeadBranch); err != nil {
- // Do not fail here as branch has already been deleted
- log.Error("DeleteBranch: %v", err)
- }
-
- ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
-}
-
-// DownloadPullDiff render a pull's raw diff
-func DownloadPullDiff(ctx *context.Context) {
- DownloadPullDiffOrPatch(ctx, false)
-}
-
-// DownloadPullPatch render a pull's raw patch
-func DownloadPullPatch(ctx *context.Context) {
- DownloadPullDiffOrPatch(ctx, true)
-}
-
-// DownloadPullDiffOrPatch render a pull's raw diff or patch
-func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
- if err != nil {
- if models.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
- } else {
- ctx.ServerError("GetIssueByIndex", err)
- }
- return
- }
-
- // Return not found if it's not a pull request
- if !issue.IsPull {
- ctx.NotFound("DownloadPullDiff",
- fmt.Errorf("Issue is not a pull request"))
- return
- }
-
- if err = issue.LoadPullRequest(); err != nil {
- ctx.ServerError("LoadPullRequest", err)
- return
- }
-
- pr := issue.PullRequest
-
- if err := pull_service.DownloadDiffOrPatch(pr, ctx, patch); err != nil {
- ctx.ServerError("DownloadDiffOrPatch", err)
- return
- }
-}
-
-// UpdatePullRequestTarget change pull request's target branch
-func UpdatePullRequestTarget(ctx *context.Context) {
- issue := GetActionIssue(ctx)
- pr := issue.PullRequest
- if ctx.Written() {
- return
- }
- if !issue.IsPull {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- targetBranch := ctx.QueryTrim("target_branch")
- if len(targetBranch) == 0 {
- ctx.Error(http.StatusNoContent)
- return
- }
-
- if err := pull_service.ChangeTargetBranch(pr, ctx.User, targetBranch); err != nil {
- if models.IsErrPullRequestAlreadyExists(err) {
- err := err.(models.ErrPullRequestAlreadyExists)
-
- RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
- errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID)
-
- ctx.Flash.Error(errorMessage)
- ctx.JSON(http.StatusConflict, map[string]interface{}{
- "error": err.Error(),
- "user_error": errorMessage,
- })
- } else if models.IsErrIssueIsClosed(err) {
- errorMessage := ctx.Tr("repo.pulls.is_closed")
-
- ctx.Flash.Error(errorMessage)
- ctx.JSON(http.StatusConflict, map[string]interface{}{
- "error": err.Error(),
- "user_error": errorMessage,
- })
- } else if models.IsErrPullRequestHasMerged(err) {
- errorMessage := ctx.Tr("repo.pulls.has_merged")
-
- ctx.Flash.Error(errorMessage)
- ctx.JSON(http.StatusConflict, map[string]interface{}{
- "error": err.Error(),
- "user_error": errorMessage,
- })
- } else if models.IsErrBranchesEqual(err) {
- errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
-
- ctx.Flash.Error(errorMessage)
- ctx.JSON(http.StatusBadRequest, map[string]interface{}{
- "error": err.Error(),
- "user_error": errorMessage,
- })
- } else {
- ctx.ServerError("UpdatePullRequestTarget", err)
- }
- return
- }
- notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, targetBranch)
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "base_branch": pr.BaseBranch,
- })
-}
+++ /dev/null
-// 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()))
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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()
- }
-}
+++ /dev/null
-// 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)
-}
-
-// Download an archive of a repository
-func Download(ctx *context.Context) {
- uri := ctx.Params("*")
- aReq := archiver_service.DeriveRequestFrom(ctx, uri)
-
- if aReq == nil {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
- complete := aReq.IsComplete()
- if !complete {
- aReq = archiver_service.ArchiveRepository(aReq)
- complete = aReq.WaitForCompletion(ctx)
- }
-
- if complete {
- ctx.ServeFile(aReq.GetArchivePath(), downloadName)
- } else {
- 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,
- })
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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"
- "io/ioutil"
- "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/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/migrations"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/mailer"
- mirror_service "code.gitea.io/gitea/services/mirror"
- repo_service "code.gitea.io/gitea/services/repository"
-)
-
-const (
- tplSettingsOptions base.TplName = "repo/settings/options"
- tplCollaboration base.TplName = "repo/settings/collaboration"
- tplBranches base.TplName = "repo/settings/branches"
- tplGithooks base.TplName = "repo/settings/githooks"
- tplGithookEdit base.TplName = "repo/settings/githook_edit"
- tplDeployKeys base.TplName = "repo/settings/deploy_keys"
- tplProtectedBranch base.TplName = "repo/settings/protected_branch"
-)
-
-// Settings show a repository's settings page
-func Settings(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsOptions"] = true
- ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
-
- signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
- ctx.Data["SigningSettings"] = setting.Repository.Signing
-
- ctx.HTML(http.StatusOK, tplSettingsOptions)
-}
-
-// SettingsPost response for changes of a repository
-func SettingsPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RepoSettingForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsOptions"] = true
-
- repo := ctx.Repo.Repository
-
- switch ctx.Query("action") {
- case "update":
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplSettingsOptions)
- return
- }
-
- newRepoName := form.RepoName
- // Check if repository name has been changed.
- if repo.LowerName != strings.ToLower(newRepoName) {
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
- if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
- ctx.Data["Err_RepoName"] = true
- switch {
- case models.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
- case models.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &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"), tplSettingsOptions, form)
- case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
- case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
- default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
- }
- case models.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- default:
- ctx.ServerError("ChangeRepositoryName", err)
- }
- return
- }
-
- log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
- }
- // In case it's just a case change.
- repo.Name = newRepoName
- repo.LowerName = strings.ToLower(newRepoName)
- repo.Description = form.Description
- repo.Website = form.Website
- repo.IsTemplate = form.Template
-
- // Visibility of forked repository is forced sync with base repository.
- if repo.IsFork {
- form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
- }
-
- visibilityChanged := repo.IsPrivate != form.Private
- // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
- if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
- ctx.ServerError("Force Private enabled", errors.New("cannot change private repository to public"))
- return
- }
-
- repo.IsPrivate = form.Private
- if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
- ctx.ServerError("UpdateRepository", 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(repo.Link() + "/settings")
-
- case "mirror":
- if !repo.IsMirror {
- ctx.NotFound("", nil)
- return
- }
-
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
-
- interval, err := time.ParseDuration(form.Interval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.Data["Err_Interval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- } else {
- ctx.Repo.Mirror.EnablePrune = form.EnablePrune
- ctx.Repo.Mirror.Interval = interval
- if interval != 0 {
- ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
- } else {
- ctx.Repo.Mirror.NextUpdateUnix = 0
- }
- if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
- ctx.Data["Err_Interval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
- }
-
- oldUsername := mirror_service.Username(ctx.Repo.Mirror)
- oldPassword := mirror_service.Password(ctx.Repo.Mirror)
- if form.MirrorPassword == "" && form.MirrorUsername == oldUsername {
- form.MirrorPassword = oldPassword
- }
-
- address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
- if err == nil {
- err = migrations.IsMigrateURLAllowed(address, ctx.User)
- }
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
-
- if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
- ctx.ServerError("UpdateAddress", err)
- return
- }
-
- form.LFS = form.LFS && setting.LFS.StartServer
-
- if 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"), tplSettingsOptions, &form)
- return
- }
- err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
- if err != nil {
- ctx.Data["Err_LFSEndpoint"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- }
-
- ctx.Repo.Mirror.LFS = form.LFS
- ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
- if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
- ctx.ServerError("UpdateMirror", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
-
- case "mirror-sync":
- if !repo.IsMirror {
- ctx.NotFound("", nil)
- return
- }
-
- mirror_service.StartToMirror(repo.ID)
-
- ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
- ctx.Redirect(repo.Link() + "/settings")
-
- case "advanced":
- var repoChanged bool
- var units []models.RepoUnit
- var deleteUnitTypes []models.UnitType
-
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
-
- if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
- repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
- repoChanged = true
- }
-
- if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalWikiURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
-
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypeExternalWiki,
- Config: &models.ExternalWikiConfig{
- ExternalWikiURL: form.ExternalWikiURL,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
- } else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypeWiki,
- Config: new(models.UnitConfig),
- })
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
- } else {
- if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
- }
- if !models.UnitTypeWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
- }
- }
-
- if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
- ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypeExternalTracker,
- Config: &models.ExternalTrackerConfig{
- ExternalTrackerURL: form.ExternalTrackerURL,
- ExternalTrackerFormat: form.TrackerURLFormat,
- ExternalTrackerStyle: form.TrackerIssueStyle,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
- } else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypeIssues,
- Config: &models.IssuesConfig{
- EnableTimetracker: form.EnableTimetracker,
- AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
- EnableDependencies: form.EnableIssueDependencies,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
- } else {
- if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
- }
- if !models.UnitTypeIssues.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
- }
- }
-
- if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypeProjects,
- })
- } else if !models.UnitTypeProjects.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
- }
-
- if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
- units = append(units, models.RepoUnit{
- RepoID: repo.ID,
- Type: models.UnitTypePullRequests,
- Config: &models.PullRequestsConfig{
- IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
- AllowMerge: form.PullsAllowMerge,
- AllowRebase: form.PullsAllowRebase,
- AllowRebaseMerge: form.PullsAllowRebaseMerge,
- AllowSquash: form.PullsAllowSquash,
- AllowManualMerge: form.PullsAllowManualMerge,
- AutodetectManualMerge: form.EnableAutodetectManualMerge,
- DefaultMergeStyle: models.MergeStyle(form.PullsDefaultMergeStyle),
- },
- })
- } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
- }
-
- if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
- ctx.ServerError("UpdateRepositoryUnits", err)
- return
- }
- if repoChanged {
- if err := models.UpdateRepository(repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
- }
- log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "signing":
- changed := false
-
- trustModel := models.ToTrustModel(form.TrustModel)
- if trustModel != repo.TrustModel {
- repo.TrustModel = trustModel
- changed = true
- }
-
- if changed {
- if err := models.UpdateRepository(repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
- }
- log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "admin":
- if !ctx.User.IsAdmin {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- if repo.IsFsckEnabled != form.EnableHealthCheck {
- repo.IsFsckEnabled = form.EnableHealthCheck
- }
-
- if err := models.UpdateRepository(repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
-
- log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "convert":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
-
- if !repo.IsMirror {
- ctx.Error(http.StatusNotFound)
- return
- }
- repo.IsMirror = false
-
- if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
- ctx.ServerError("CleanUpMigrateInfo", err)
- return
- } else if err = models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("DeleteMirrorByRepoID", err)
- return
- }
- log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
- ctx.Redirect(repo.Link())
-
- case "convert_fork":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if err := repo.GetOwner(); err != nil {
- ctx.ServerError("Convert Fork", err)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
-
- if !repo.IsFork {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- if !ctx.Repo.Owner.CanCreateRepo() {
- ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
-
- repo.IsFork = false
- repo.ForkID = 0
- if err := models.UpdateRepository(repo, false); err != nil {
- log.Error("Unable to update repository %-v whilst converting from fork", repo)
- ctx.ServerError("Convert Fork", err)
- return
- }
-
- log.Trace("Repository converted from fork to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
- ctx.Redirect(repo.Link())
-
- case "transfer":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
-
- newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- ctx.ServerError("IsUserExist", err)
- return
- }
-
- if newOwner.Type == models.UserTypeOrganization {
- if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !newOwner.HasMemberWithUserID(ctx.User.ID) {
- // The user shouldn't know about this organization
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- }
-
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
-
- if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
- if models.IsErrRepoAlreadyExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
- } else if models.IsErrRepoTransferInProgress(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
- } else {
- ctx.ServerError("TransferOwnership", err)
- }
-
- return
- }
-
- log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
- ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
-
- case "cancel_transfer":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
- if err != nil {
- if models.IsErrNoPendingTransfer(err) {
- ctx.Flash.Error("repo.settings.transfer_abort_invalid")
- ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings")
- } else {
- ctx.ServerError("GetPendingRepositoryTransfer", err)
- }
-
- return
- }
-
- if err := repoTransfer.LoadAttributes(); err != nil {
- ctx.ServerError("LoadRecipient", err)
- return
- }
-
- if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
- ctx.ServerError("CancelRepositoryTransfer", err)
- return
- }
-
- log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
- ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
-
- case "delete":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
-
- // Close the gitrepository before doing this.
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
-
- if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
- ctx.ServerError("DeleteRepository", err)
- return
- }
- log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
- ctx.Redirect(ctx.Repo.Owner.DashboardLink())
-
- case "delete-wiki":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
-
- err := repo.DeleteWiki()
- if err != nil {
- log.Error("Delete Wiki: %v", err.Error())
- }
- log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "archive":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- if repo.IsMirror {
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
-
- if err := repo.SetArchiveRepoState(true); err != nil {
- log.Error("Tried to archive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
-
- log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- case "unarchive":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- if err := repo.SetArchiveRepoState(false); err != nil {
- log.Error("Tried to unarchive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
-
- log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- default:
- ctx.NotFound("", nil)
- }
-}
-
-func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
- if models.IsErrInvalidCloneAddr(err) {
- addrErr := err.(*models.ErrInvalidCloneAddr)
- switch {
- case addrErr.IsProtocolInvalid:
- ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
- case addrErr.IsURLError:
- ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
- case addrErr.IsPermissionDenied:
- if addrErr.LocalPath {
- ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
- } else if len(addrErr.PrivateNet) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
- } else {
- ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tplSettingsOptions, form)
- }
- case addrErr.IsInvalidPath:
- ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
- default:
- ctx.ServerError("Unknown error", err)
- }
- }
- ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
-}
-
-// Collaboration render a repository's collaboration page
-func Collaboration(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsCollaboration"] = true
-
- users, err := ctx.Repo.Repository.GetCollaborators(models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetCollaborators", err)
- return
- }
- ctx.Data["Collaborators"] = users
-
- teams, err := ctx.Repo.Repository.GetRepoTeams()
- if err != nil {
- ctx.ServerError("GetRepoTeams", err)
- return
- }
- ctx.Data["Teams"] = teams
- ctx.Data["Repo"] = ctx.Repo.Repository
- ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
- ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
- ctx.Data["Org"] = ctx.Repo.Repository.Owner
- ctx.Data["Units"] = models.Units
-
- ctx.HTML(http.StatusOK, tplCollaboration)
-}
-
-// CollaborationPost response for actions for a collaboration of a repository
-func CollaborationPost(ctx *context.Context) {
- name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
- if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
- ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
- return
- }
-
- u, err := models.GetUserByName(name)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
- ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
- } else {
- ctx.ServerError("GetUserByName", err)
- }
- return
- }
-
- if !u.IsActive {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
- ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
- return
- }
-
- // Organization is not allowed to be added as a collaborator.
- if u.IsOrganization() {
- ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
- ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
- return
- }
-
- if got, err := ctx.Repo.Repository.IsCollaborator(u.ID); err == nil && got {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- if err = ctx.Repo.Repository.AddCollaborator(u); err != nil {
- ctx.ServerError("AddCollaborator", err)
- return
- }
-
- if setting.Service.EnableNotifyMail {
- mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
- ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
-}
-
-// ChangeCollaborationAccessMode response for changing access of a collaboration
-func ChangeCollaborationAccessMode(ctx *context.Context) {
- if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
- ctx.QueryInt64("uid"),
- models.AccessMode(ctx.QueryInt("mode"))); err != nil {
- log.Error("ChangeCollaborationAccessMode: %v", err)
- }
-}
-
-// DeleteCollaboration delete a collaboration for a repository
-func DeleteCollaboration(ctx *context.Context) {
- if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteCollaboration: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
- })
-}
-
-// AddTeamPost response for adding a team to a repository
-func AddTeamPost(ctx *context.Context) {
- if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
- ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
- if len(name) == 0 {
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- team, err := ctx.Repo.Owner.GetTeam(name)
- if err != nil {
- if models.IsErrTeamNotExist(err) {
- ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- } else {
- ctx.ServerError("GetTeam", err)
- }
- return
- }
-
- if team.OrgID != ctx.Repo.Repository.OwnerID {
- ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- if err = team.AddRepository(ctx.Repo.Repository); err != nil {
- ctx.ServerError("team.AddRepository", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
-}
-
-// DeleteTeam response for deleting a team from a repository
-func DeleteTeam(ctx *context.Context) {
- if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
- ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
- return
- }
-
- team, err := models.GetTeamByID(ctx.QueryInt64("id"))
- if err != nil {
- ctx.ServerError("GetTeamByID", err)
- return
- }
-
- if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("team.RemoveRepositorys", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
- })
-}
-
-// parseOwnerAndRepo get repos by owner
-func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
- owner, err := models.GetUserByName(ctx.Params(":username"))
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.NotFound("GetUserByName", err)
- } else {
- ctx.ServerError("GetUserByName", err)
- }
- return nil, nil
- }
-
- repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
- if err != nil {
- if models.IsErrRepoNotExist(err) {
- ctx.NotFound("GetRepositoryByName", err)
- } else {
- ctx.ServerError("GetRepositoryByName", err)
- }
- return nil, nil
- }
-
- return owner, repo
-}
-
-// GitHooks hooks of a repository
-func GitHooks(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
- ctx.Data["PageIsSettingsGitHooks"] = true
-
- hooks, err := ctx.Repo.GitRepo.Hooks()
- if err != nil {
- ctx.ServerError("Hooks", err)
- return
- }
- ctx.Data["Hooks"] = hooks
-
- ctx.HTML(http.StatusOK, tplGithooks)
-}
-
-// GitHooksEdit render for editing a hook of repository page
-func GitHooksEdit(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
- ctx.Data["PageIsSettingsGitHooks"] = true
-
- name := ctx.Params(":name")
- hook, err := ctx.Repo.GitRepo.GetHook(name)
- if err != nil {
- if err == git.ErrNotValidHook {
- ctx.NotFound("GetHook", err)
- } else {
- ctx.ServerError("GetHook", err)
- }
- return
- }
- ctx.Data["Hook"] = hook
- ctx.HTML(http.StatusOK, tplGithookEdit)
-}
-
-// GitHooksEditPost response for editing a git hook of a repository
-func GitHooksEditPost(ctx *context.Context) {
- name := ctx.Params(":name")
- hook, err := ctx.Repo.GitRepo.GetHook(name)
- if err != nil {
- if err == git.ErrNotValidHook {
- ctx.NotFound("GetHook", err)
- } else {
- ctx.ServerError("GetHook", err)
- }
- return
- }
- hook.Content = ctx.Query("content")
- if err = hook.Update(); err != nil {
- ctx.ServerError("hook.Update", err)
- return
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
-}
-
-// DeployKeys render the deploy keys list of a repository page
-func DeployKeys(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
- ctx.Data["PageIsSettingsKeys"] = true
- ctx.Data["DisableSSH"] = setting.SSH.Disabled
-
- keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
- if err != nil {
- ctx.ServerError("ListDeployKeys", err)
- return
- }
- ctx.Data["Deploykeys"] = keys
-
- ctx.HTML(http.StatusOK, tplDeployKeys)
-}
-
-// DeployKeysPost response for adding a deploy key of a repository
-func DeployKeysPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.AddKeyForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
- ctx.Data["PageIsSettingsKeys"] = true
-
- keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
- if err != nil {
- ctx.ServerError("ListDeployKeys", err)
- return
- }
- ctx.Data["Deploykeys"] = keys
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplDeployKeys)
- return
- }
-
- 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.Data["HasError"] = true
- ctx.Data["Err_Content"] = true
- ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
- return
- }
-
- key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
- if err != nil {
- ctx.Data["HasError"] = true
- switch {
- case models.IsErrDeployKeyAlreadyExist(err):
- ctx.Data["Err_Content"] = true
- ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
- case models.IsErrKeyAlreadyExist(err):
- ctx.Data["Err_Content"] = true
- ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
- case models.IsErrKeyNameAlreadyUsed(err):
- ctx.Data["Err_Title"] = true
- ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
- case models.IsErrDeployKeyNameAlreadyUsed(err):
- ctx.Data["Err_Title"] = true
- ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
- default:
- ctx.ServerError("AddDeployKey", err)
- }
- return
- }
-
- log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
- ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
-}
-
-// DeleteDeployKey response for deleting a deploy key
-func DeleteDeployKey(ctx *context.Context) {
- if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteDeployKey: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": ctx.Repo.RepoLink + "/settings/keys",
- })
-}
-
-// UpdateAvatarSetting update repo's avatar
-func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
- ctxRepo := ctx.Repo.Repository
-
- if form.Avatar == nil {
- // No avatar is uploaded and we not removing it here.
- // No random avatar generated here.
- // Just exit, no action.
- if ctxRepo.CustomAvatarRelativePath() == "" {
- log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
- }
- return nil
- }
-
- r, err := form.Avatar.Open()
- if err != nil {
- return fmt.Errorf("Avatar.Open: %v", err)
- }
- defer r.Close()
-
- if form.Avatar.Size > setting.Avatar.MaxFileSize {
- return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
- }
-
- data, err := ioutil.ReadAll(r)
- 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 = ctxRepo.UploadAvatar(data); err != nil {
- return fmt.Errorf("UploadAvatar: %v", err)
- }
- return nil
-}
-
-// SettingsAvatar save new POSTed repository avatar
-func SettingsAvatar(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.AvatarForm)
- form.Source = forms.AvatarLocal
- if err := UpdateAvatarSetting(ctx, *form); err != nil {
- ctx.Flash.Error(err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-}
-
-// SettingsDeleteAvatar delete repository avatar
-func SettingsDeleteAvatar(ctx *context.Context) {
- if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
- ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
- }
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-}
+++ /dev/null
-// 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))
- }
-}
+++ /dev/null
-// 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))
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// Copyright 2015 The Gogs Authors. All rights reserved.
-// 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 (
- "errors"
- "fmt"
- "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/convert"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook"
- jsoniter "github.com/json-iterator/go"
-)
-
-const (
- tplHooks base.TplName = "repo/settings/webhook/base"
- tplHookNew base.TplName = "repo/settings/webhook/new"
- tplOrgHookNew base.TplName = "org/settings/hook_new"
- tplAdminHookNew base.TplName = "admin/hook_new"
-)
-
-// Webhooks render web hooks list page
-func Webhooks(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.hooks")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
- ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
- ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/")
-
- ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, models.ListOptions{})
- if err != nil {
- ctx.ServerError("GetWebhooksByRepoID", err)
- return
- }
- ctx.Data["Webhooks"] = ws
-
- ctx.HTML(http.StatusOK, tplHooks)
-}
-
-type orgRepoCtx struct {
- OrgID int64
- RepoID int64
- IsAdmin bool
- IsSystemWebhook bool
- Link string
- LinkNew string
- NewTemplate base.TplName
-}
-
-// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context.
-func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
- if len(ctx.Repo.RepoLink) > 0 {
- return &orgRepoCtx{
- RepoID: ctx.Repo.Repository.ID,
- Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
- LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
- NewTemplate: tplHookNew,
- }, nil
- }
-
- if len(ctx.Org.OrgLink) > 0 {
- return &orgRepoCtx{
- OrgID: ctx.Org.Organization.ID,
- Link: path.Join(ctx.Org.OrgLink, "settings/hooks"),
- LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"),
- NewTemplate: tplOrgHookNew,
- }, nil
- }
-
- if ctx.User.IsAdmin {
- // Are we looking at default webhooks?
- if ctx.Params(":configType") == "default-hooks" {
- return &orgRepoCtx{
- IsAdmin: true,
- Link: path.Join(setting.AppSubURL, "/admin/hooks"),
- LinkNew: path.Join(setting.AppSubURL, "/admin/default-hooks"),
- NewTemplate: tplAdminHookNew,
- }, nil
- }
-
- // Must be system webhooks instead
- return &orgRepoCtx{
- IsAdmin: true,
- IsSystemWebhook: true,
- Link: path.Join(setting.AppSubURL, "/admin/hooks"),
- LinkNew: path.Join(setting.AppSubURL, "/admin/system-hooks"),
- NewTemplate: tplAdminHookNew,
- }, nil
- }
-
- return nil, errors.New("Unable to set OrgRepo context")
-}
-
-func checkHookType(ctx *context.Context) string {
- hookType := strings.ToLower(ctx.Params(":type"))
- if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) {
- ctx.NotFound("checkHookType", nil)
- return ""
- }
- return hookType
-}
-
-// WebhooksNew render creating webhook page
-func WebhooksNew(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if orCtx.IsAdmin && orCtx.IsSystemWebhook {
- ctx.Data["PageIsAdminSystemHooks"] = true
- ctx.Data["PageIsAdminSystemHooksNew"] = true
- } else if orCtx.IsAdmin {
- ctx.Data["PageIsAdminDefaultHooks"] = true
- ctx.Data["PageIsAdminDefaultHooksNew"] = true
- } else {
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- }
-
- hookType := checkHookType(ctx)
- ctx.Data["HookType"] = hookType
- if ctx.Written() {
- return
- }
- if hookType == "discord" {
- ctx.Data["DiscordHook"] = map[string]interface{}{
- "Username": "Gitea",
- "IconURL": setting.AppURL + "img/favicon.png",
- }
- }
- ctx.Data["BaseLink"] = orCtx.LinkNew
-
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-}
-
-// ParseHookEvent convert web form content to models.HookEvent
-func ParseHookEvent(form forms.WebhookForm) *models.HookEvent {
- return &models.HookEvent{
- PushOnly: form.PushOnly(),
- SendEverything: form.SendEverything(),
- ChooseEvents: form.ChooseEvents(),
- HookEvents: models.HookEvents{
- Create: form.Create,
- Delete: form.Delete,
- Fork: form.Fork,
- Issues: form.Issues,
- IssueAssign: form.IssueAssign,
- IssueLabel: form.IssueLabel,
- IssueMilestone: form.IssueMilestone,
- IssueComment: form.IssueComment,
- Release: form.Release,
- Push: form.Push,
- PullRequest: form.PullRequest,
- PullRequestAssign: form.PullRequestAssign,
- PullRequestLabel: form.PullRequestLabel,
- PullRequestMilestone: form.PullRequestMilestone,
- PullRequestComment: form.PullRequestComment,
- PullRequestReview: form.PullRequestReview,
- PullRequestSync: form.PullRequestSync,
- Repository: form.Repository,
- },
- BranchFilter: form.BranchFilter,
- }
-}
-
-// GiteaHooksNewPost response for creating Gitea webhook
-func GiteaHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWebhookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.GITEA
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
- ctx.Data["BaseLink"] = orCtx.LinkNew
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := models.ContentTypeJSON
- if models.HookContentType(form.ContentType) == models.ContentTypeForm {
- contentType = models.ContentTypeForm
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- HTTPMethod: form.HTTPMethod,
- ContentType: contentType,
- Secret: form.Secret,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.GITEA,
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// GogsHooksNewPost response for creating webhook
-func GogsHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewGogshookForm)
- newGogsWebhookPost(ctx, *form, models.GOGS)
-}
-
-// newGogsWebhookPost response for creating gogs hook
-func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind models.HookTaskType) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.GOGS
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
- ctx.Data["BaseLink"] = orCtx.LinkNew
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := models.ContentTypeJSON
- if models.HookContentType(form.ContentType) == models.ContentTypeForm {
- contentType = models.ContentTypeForm
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: contentType,
- Secret: form.Secret,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: kind,
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// DiscordHooksNewPost response for creating discord hook
-func DiscordHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.DISCORD
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.DiscordMeta{
- Username: form.Username,
- IconURL: form.IconURL,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.DISCORD,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// DingtalkHooksNewPost response for creating dingtalk hook
-func DingtalkHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.DINGTALK
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.DINGTALK,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// TelegramHooksNewPost response for creating telegram hook
-func TelegramHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.TELEGRAM
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.TelegramMeta{
- BotToken: form.BotToken,
- ChatID: form.ChatID,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID),
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.TELEGRAM,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// MatrixHooksNewPost response for creating a Matrix hook
-func MatrixHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.MATRIX
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.MatrixMeta{
- HomeserverURL: form.HomeserverURL,
- Room: form.RoomID,
- AccessToken: form.AccessToken,
- MessageType: form.MessageType,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID),
- ContentType: models.ContentTypeJSON,
- HTTPMethod: "PUT",
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.MATRIX,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// MSTeamsHooksNewPost response for creating MS Teams hook
-func MSTeamsHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.MSTEAMS
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.MSTEAMS,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// SlackHooksNewPost response for creating slack hook
-func SlackHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewSlackHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.SLACK
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- if form.HasInvalidChannel() {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
- ctx.Redirect(orCtx.LinkNew + "/slack/new")
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.SlackMeta{
- Channel: strings.TrimSpace(form.Channel),
- Username: form.Username,
- IconURL: form.IconURL,
- Color: form.Color,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.SLACK,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// FeishuHooksNewPost response for creating feishu hook
-func FeishuHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
- ctx.Data["HookType"] = models.FEISHU
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &models.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: models.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: models.FEISHU,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.CreateWebhook(w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
- ctx.Data["RequireHighlightJS"] = true
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return nil, nil
- }
- ctx.Data["BaseLink"] = orCtx.Link
-
- var w *models.Webhook
- if orCtx.RepoID > 0 {
- w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
- } else if orCtx.OrgID > 0 {
- w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
- } else if orCtx.IsAdmin {
- w, err = models.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
- }
- if err != nil || w == nil {
- if models.IsErrWebhookNotExist(err) {
- ctx.NotFound("GetWebhookByID", nil)
- } else {
- ctx.ServerError("GetWebhookByID", err)
- }
- return nil, nil
- }
-
- ctx.Data["HookType"] = w.Type
- switch w.Type {
- case models.SLACK:
- ctx.Data["SlackHook"] = webhook.GetSlackHook(w)
- case models.DISCORD:
- ctx.Data["DiscordHook"] = webhook.GetDiscordHook(w)
- case models.TELEGRAM:
- ctx.Data["TelegramHook"] = webhook.GetTelegramHook(w)
- case models.MATRIX:
- ctx.Data["MatrixHook"] = webhook.GetMatrixHook(w)
- }
-
- ctx.Data["History"], err = w.History(1)
- if err != nil {
- ctx.ServerError("History", err)
- }
- return orCtx, w
-}
-
-// WebHooksEdit render editing web hook page
-func WebHooksEdit(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-}
-
-// WebHooksEditPost response for editing web hook
-func WebHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWebhookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := models.ContentTypeJSON
- if models.HookContentType(form.ContentType) == models.ContentTypeForm {
- contentType = models.ContentTypeForm
- }
-
- w.URL = form.PayloadURL
- w.ContentType = contentType
- w.Secret = form.Secret
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- w.HTTPMethod = form.HTTPMethod
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("WebHooksEditPost", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// GogsHooksEditPost response for editing gogs hook
-func GogsHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewGogshookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := models.ContentTypeJSON
- if models.HookContentType(form.ContentType) == models.ContentTypeForm {
- contentType = models.ContentTypeForm
- }
-
- w.URL = form.PayloadURL
- w.ContentType = contentType
- w.Secret = form.Secret
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("GogsHooksEditPost", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// SlackHooksEditPost response for editing slack hook
-func SlackHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewSlackHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- if form.HasInvalidChannel() {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.SlackMeta{
- Channel: strings.TrimSpace(form.Channel),
- Username: form.Username,
- IconURL: form.IconURL,
- Color: form.Color,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w.URL = form.PayloadURL
- w.Meta = string(meta)
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DiscordHooksEditPost response for editing discord hook
-func DiscordHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.DiscordMeta{
- Username: form.Username,
- IconURL: form.IconURL,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w.URL = form.PayloadURL
- w.Meta = string(meta)
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DingtalkHooksEditPost response for editing discord hook
-func DingtalkHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// TelegramHooksEditPost response for editing discord hook
-func TelegramHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.TelegramMeta{
- BotToken: form.BotToken,
- ChatID: form.ChatID,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
- w.Meta = string(meta)
- w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID)
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// MatrixHooksEditPost response for editing a Matrix hook
-func MatrixHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
- json := jsoniter.ConfigCompatibleWithStandardLibrary
- meta, err := json.Marshal(&webhook.MatrixMeta{
- HomeserverURL: form.HomeserverURL,
- Room: form.RoomID,
- AccessToken: form.AccessToken,
- MessageType: form.MessageType,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
- w.Meta = string(meta)
- w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID)
-
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// MSTeamsHooksEditPost response for editing MS Teams hook
-func MSTeamsHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// FeishuHooksEditPost response for editing feishu hook
-func FeishuHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := models.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// TestWebhook test if web hook is work fine
-func TestWebhook(ctx *context.Context) {
- hookID := ctx.ParamsInt64(":id")
- w, err := models.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID)
- if err != nil {
- ctx.Flash.Error("GetWebhookByID: " + err.Error())
- ctx.Status(500)
- return
- }
-
- // Grab latest commit or fake one if it's empty repository.
- commit := ctx.Repo.Commit
- if commit == nil {
- ghost := models.NewGhostUser()
- commit = &git.Commit{
- ID: git.MustIDFromString(git.EmptySHA),
- Author: ghost.NewGitSig(),
- Committer: ghost.NewGitSig(),
- CommitMessage: "This is a fake commit",
- }
- }
-
- apiUser := convert.ToUserWithAccessMode(ctx.User, models.AccessModeNone)
- p := &api.PushPayload{
- Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
- Before: commit.ID.String(),
- After: commit.ID.String(),
- Commits: []*api.PayloadCommit{
- {
- ID: commit.ID.String(),
- Message: commit.Message(),
- URL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
- Author: &api.PayloadUser{
- Name: commit.Author.Name,
- Email: commit.Author.Email,
- },
- Committer: &api.PayloadUser{
- Name: commit.Committer.Name,
- Email: commit.Committer.Email,
- },
- },
- },
- Repo: convert.ToRepo(ctx.Repo.Repository, models.AccessModeNone),
- Pusher: apiUser,
- Sender: apiUser,
- }
- if err := webhook.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil {
- ctx.Flash.Error("PrepareWebhook: " + err.Error())
- ctx.Status(500)
- } else {
- ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success"))
- ctx.Status(200)
- }
-}
-
-// DeleteWebhook delete a webhook
-func DeleteWebhook(ctx *context.Context) {
- if err := models.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
- ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
- } else {
- ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
- }
-
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": ctx.Repo.RepoLink + "/settings/hooks",
- })
-}
+++ /dev/null
-// 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/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 = 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/",
- })
-}
+++ /dev/null
-// 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"))
- }
-}
+++ /dev/null
-// 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 routes
-
-import (
- "errors"
- "fmt"
- "io"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
-
- "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"
-)
-
-// LoggerHandler is a handler that will log the routing to the default gitea log
-func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
- return func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- start := time.Now()
-
- _ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
-
- next.ServeHTTP(w, req)
-
- var status int
- if v, ok := w.(context.ResponseWriter); ok {
- status = v.Status()
- }
-
- _ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
- })
- }
-}
-
-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 struct {
- Data map[string]interface{}
-}
-
-func (d *dataStore) GetData() map[string]interface{} {
- return d.Data
-}
-
-// 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{
- Data: templates.Vars{
- "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.Data["IsSigned"] = true
- store.Data["SignedUser"] = user
- store.Data["SignedUserID"] = user.ID
- store.Data["SignedUserName"] = user.Name
- store.Data["IsAdmin"] = user.IsAdmin
- } else {
- store.Data["SignedUserID"] = int64(0)
- store.Data["SignedUserName"] = ""
- }
-
- w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
-
- if !setting.IsProd() {
- store.Data["ErrorMsg"] = combinedErr
- }
- err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
- if err != nil {
- log.Error("%v", err)
- }
- }
- }()
-
- next.ServeHTTP(w, req)
- })
- }
-}
+++ /dev/null
-// 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 routes
-
-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,
- })))
-}
+++ /dev/null
-// 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 routes
-
-import (
- "fmt"
- "net/http"
- "path"
-
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/public"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers"
- "code.gitea.io/gitea/services/forms"
-
- "gitea.com/go-chi/session"
-)
-
-func installRecovery() 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() {
- // Why we need this? The first recover will try to render a beautiful
- // error page for user, but the process can still panic again, then
- // we have to just recover twice and send a simple error page that
- // should not panic any more.
- defer func() {
- if err := recover(); err != nil {
- combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
- log.Error(combinedErr)
- if setting.IsProd() {
- http.Error(w, http.StatusText(500), 500)
- } else {
- http.Error(w, combinedErr, 500)
- }
- }
- }()
-
- if err := recover(); err != nil {
- combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
- log.Error("%v", combinedErr)
-
- lc := middleware.Locale(w, req)
- var store = dataStore{
- Data: templates.Vars{
- "Language": lc.Language(),
- "CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
- "i18n": lc,
- "SignedUserID": int64(0),
- "SignedUserName": "",
- },
- }
-
- w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
-
- if !setting.IsProd() {
- store.Data["ErrorMsg"] = combinedErr
- }
- err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
- if err != nil {
- log.Error("%v", err)
- }
- }
- }()
-
- next.ServeHTTP(w, req)
- })
- }
-}
-
-// InstallRoutes registers the install routes
-func InstallRoutes() *web.Route {
- r := web.NewRoute()
- for _, middle := range commonMiddlewares() {
- r.Use(middle)
- }
-
- r.Use(public.AssetsHandler(&public.Options{
- Directory: path.Join(setting.StaticRootPath, "public"),
- Prefix: "/assets",
- }))
-
- r.Use(session.Sessioner(session.Options{
- Provider: setting.SessionConfig.Provider,
- ProviderConfig: setting.SessionConfig.ProviderConfig,
- CookieName: setting.SessionConfig.CookieName,
- CookiePath: setting.SessionConfig.CookiePath,
- Gclifetime: setting.SessionConfig.Gclifetime,
- Maxlifetime: setting.SessionConfig.Maxlifetime,
- Secure: setting.SessionConfig.Secure,
- SameSite: setting.SessionConfig.SameSite,
- Domain: setting.SessionConfig.Domain,
- }))
-
- r.Use(installRecovery())
- r.Use(routers.InstallInit)
- r.Get("/", routers.Install)
- r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost)
- r.NotFound(func(w http.ResponseWriter, req *http.Request) {
- http.Redirect(w, req, setting.AppURL, http.StatusFound)
- })
- return r
-}
+++ /dev/null
-// 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 routes
-
-import (
- "encoding/gob"
- "fmt"
- "net/http"
- "os"
- "path"
- "strings"
-
- "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/metrics"
- "code.gitea.io/gitea/modules/public"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers"
- "code.gitea.io/gitea/routers/admin"
- apiv1 "code.gitea.io/gitea/routers/api/v1"
- "code.gitea.io/gitea/routers/api/v1/misc"
- "code.gitea.io/gitea/routers/dev"
- "code.gitea.io/gitea/routers/events"
- "code.gitea.io/gitea/routers/org"
- "code.gitea.io/gitea/routers/private"
- "code.gitea.io/gitea/routers/repo"
- "code.gitea.io/gitea/routers/user"
- userSetting "code.gitea.io/gitea/routers/user/setting"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/lfs"
- "code.gitea.io/gitea/services/mailer"
-
- // to registers all internal adapters
- _ "code.gitea.io/gitea/modules/session"
-
- "gitea.com/go-chi/captcha"
- "gitea.com/go-chi/session"
- "github.com/NYTimes/gziphandler"
- "github.com/chi-middleware/proxy"
- "github.com/go-chi/chi/middleware"
- "github.com/go-chi/cors"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/tstranex/u2f"
-)
-
-const (
- // GzipMinSize represents min size to compress for the body size of response
- GzipMinSize = 1400
-)
-
-func commonMiddlewares() []func(http.Handler) http.Handler {
- var handlers = []func(http.Handler) http.Handler{
- func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- next.ServeHTTP(context.NewResponse(resp), req)
- })
- },
- }
-
- if setting.ReverseProxyLimit > 0 {
- opt := proxy.NewForwardedHeadersOptions().
- WithForwardLimit(setting.ReverseProxyLimit).
- ClearTrustedProxies()
- for _, n := range setting.ReverseProxyTrustedProxies {
- if !strings.Contains(n, "/") {
- opt.AddTrustedProxy(n)
- } else {
- opt.AddTrustedNetwork(n)
- }
- }
- handlers = append(handlers, proxy.ForwardedHeaders(opt))
- }
-
- handlers = append(handlers, middleware.StripSlashes)
-
- if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
- if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
- handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
- }
- }
- if setting.EnableAccessLog {
- handlers = append(handlers, context.AccessLogger())
- }
-
- handlers = append(handlers, func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- // Why we need this? The Recovery() will try to render a beautiful
- // error page for user, but the process can still panic again, and other
- // middleware like session also may panic then we have to recover twice
- // and send a simple error page that should not panic any more.
- defer func() {
- if err := recover(); err != nil {
- combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
- log.Error("%v", combinedErr)
- if setting.IsProd() {
- http.Error(resp, http.StatusText(500), 500)
- } else {
- http.Error(resp, combinedErr, 500)
- }
- }
- }()
- next.ServeHTTP(resp, req)
- })
- })
- return handlers
-}
-
-var corsHandler func(http.Handler) http.Handler
-
-// NormalRoutes represents non install routes
-func NormalRoutes() *web.Route {
- r := web.NewRoute()
- for _, middle := range commonMiddlewares() {
- r.Use(middle)
- }
-
- if setting.CORSConfig.Enabled {
- corsHandler = cors.Handler(cors.Options{
- //Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
- AllowedOrigins: setting.CORSConfig.AllowDomain,
- //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
- AllowedMethods: setting.CORSConfig.Methods,
- AllowCredentials: setting.CORSConfig.AllowCredentials,
- MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
- })
- } else {
- corsHandler = func(next http.Handler) http.Handler {
- return next
- }
- }
-
- r.Mount("/", WebRoutes())
- r.Mount("/api/v1", apiv1.Routes())
- r.Mount("/api/internal", private.Routes())
- return r
-}
-
-// WebRoutes returns all web routes
-func WebRoutes() *web.Route {
- routes := web.NewRoute()
-
- routes.Use(public.AssetsHandler(&public.Options{
- Directory: path.Join(setting.StaticRootPath, "public"),
- Prefix: "/assets",
- CorsHandler: corsHandler,
- }))
-
- routes.Use(session.Sessioner(session.Options{
- Provider: setting.SessionConfig.Provider,
- ProviderConfig: setting.SessionConfig.ProviderConfig,
- CookieName: setting.SessionConfig.CookieName,
- CookiePath: setting.SessionConfig.CookiePath,
- Gclifetime: setting.SessionConfig.Gclifetime,
- Maxlifetime: setting.SessionConfig.Maxlifetime,
- Secure: setting.SessionConfig.Secure,
- SameSite: setting.SessionConfig.SameSite,
- Domain: setting.SessionConfig.Domain,
- }))
-
- routes.Use(Recovery())
-
- // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
- routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
- routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
-
- // for health check - doeesn't need to be passed through gzip handler
- routes.Head("/", func(w http.ResponseWriter, req *http.Request) {
- w.WriteHeader(http.StatusOK)
- })
-
- // this png is very likely to always be below the limit for gzip so it doesn't need to pass through gzip
- routes.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) {
- http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301)
- })
-
- gob.Register(&u2f.Challenge{})
-
- common := []interface{}{}
-
- if setting.EnableGzip {
- h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
- if err != nil {
- log.Fatal("GzipHandlerWithOpts failed: %v", err)
- }
- common = append(common, h)
- }
-
- mailer.InitMailRender(templates.Mailer())
-
- if setting.Service.EnableCaptcha {
- // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
- routes.Route("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...)
- }
-
- if setting.HasRobotsTxt {
- routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) {
- filePath := path.Join(setting.CustomPath, "robots.txt")
- fi, err := os.Stat(filePath)
- if err == nil && httpcache.HandleTimeCache(req, w, fi) {
- return
- }
- http.ServeFile(w, req, filePath)
- })...)
- }
-
- // prometheus metrics endpoint - do not need to go through contexter
- if setting.Metrics.Enabled {
- c := metrics.NewCollector()
- prometheus.MustRegister(c)
-
- routes.Get("/metrics", append(common, routers.Metrics)...)
- }
-
- // Removed: toolbox.Toolboxer middleware will provide debug informations which seems unnecessary
- common = append(common, context.Contexter())
-
- // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
- common = append(common, middleware.GetHead)
-
- if setting.API.EnableSwagger {
- // Note: The route moved from apiroutes because it's in fact want to render a web page
- routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default
- }
-
- // TODO: These really seem like things that could be folded into Contexter or as helper functions
- common = append(common, user.GetNotificationCount)
- common = append(common, repo.GetActiveStopwatch)
- common = append(common, goGet)
-
- others := web.NewRoute()
- for _, middle := range common {
- others.Use(middle)
- }
-
- RegisterRoutes(others)
- routes.Mount("", others)
- return routes
-}
-
-// RegisterRoutes register routes
-func RegisterRoutes(m *web.Route) {
- reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
- ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
- ignExploreSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
- ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
- reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
-
- //bindIgnErr := binding.BindIgnErr
- bindIgnErr := web.Bind
- validation.AddBindingRules()
-
- openIDSignInEnabled := func(ctx *context.Context) {
- if !setting.Service.EnableOpenIDSignIn {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- openIDSignUpEnabled := func(ctx *context.Context) {
- if !setting.Service.EnableOpenIDSignUp {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
- if !setting.Service.ShowMilestonesDashboardPage {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- // webhooksEnabled requires webhooks to be enabled by admin.
- webhooksEnabled := func(ctx *context.Context) {
- if setting.DisableWebhooks {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- lfsServerEnabled := func(ctx *context.Context) {
- if !setting.LFS.StartServer {
- ctx.Error(http.StatusNotFound)
- return
- }
- }
-
- // FIXME: not all routes need go through same middleware.
- // Especially some AJAX requests, we can reduce middleware number to improve performance.
- // Routers.
- // for health check
- m.Get("/", routers.Home)
- m.Get("/.well-known/openid-configuration", user.OIDCWellKnown)
- m.Group("/explore", func() {
- m.Get("", func(ctx *context.Context) {
- ctx.Redirect(setting.AppSubURL + "/explore/repos")
- })
- m.Get("/repos", routers.ExploreRepos)
- m.Get("/users", routers.ExploreUsers)
- m.Get("/organizations", routers.ExploreOrganizations)
- m.Get("/code", routers.ExploreCode)
- }, ignExploreSignIn)
- m.Get("/issues", reqSignIn, user.Issues)
- m.Get("/pulls", reqSignIn, user.Pulls)
- m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
-
- // ***** START: User *****
- m.Group("/user", func() {
- m.Get("/login", user.SignIn)
- m.Post("/login", bindIgnErr(forms.SignInForm{}), user.SignInPost)
- m.Group("", func() {
- m.Combo("/login/openid").
- Get(user.SignInOpenID).
- Post(bindIgnErr(forms.SignInOpenIDForm{}), user.SignInOpenIDPost)
- }, openIDSignInEnabled)
- m.Group("/openid", func() {
- m.Combo("/connect").
- Get(user.ConnectOpenID).
- Post(bindIgnErr(forms.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
- m.Group("/register", func() {
- m.Combo("").
- Get(user.RegisterOpenID, openIDSignUpEnabled).
- Post(bindIgnErr(forms.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
- }, openIDSignUpEnabled)
- }, openIDSignInEnabled)
- m.Get("/sign_up", user.SignUp)
- m.Post("/sign_up", bindIgnErr(forms.RegisterForm{}), user.SignUpPost)
- m.Group("/oauth2", func() {
- m.Get("/{provider}", user.SignInOAuth)
- m.Get("/{provider}/callback", user.SignInOAuthCallback)
- })
- m.Get("/link_account", user.LinkAccount)
- m.Post("/link_account_signin", bindIgnErr(forms.SignInForm{}), user.LinkAccountPostSignIn)
- m.Post("/link_account_signup", bindIgnErr(forms.RegisterForm{}), user.LinkAccountPostRegister)
- m.Group("/two_factor", func() {
- m.Get("", user.TwoFactor)
- m.Post("", bindIgnErr(forms.TwoFactorAuthForm{}), user.TwoFactorPost)
- m.Get("/scratch", user.TwoFactorScratch)
- m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
- })
- m.Group("/u2f", func() {
- m.Get("", user.U2F)
- m.Get("/challenge", user.U2FChallenge)
- m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
-
- })
- }, reqSignOut)
-
- m.Any("/user/events", events.Events)
-
- m.Group("/login/oauth", func() {
- m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
- m.Post("/grant", bindIgnErr(forms.GrantApplicationForm{}), user.GrantApplicationOAuth)
- // TODO manage redirection
- m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
- }, ignSignInAndCsrf, reqSignIn)
- m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
- m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
-
- m.Group("/user/settings", func() {
- m.Get("", userSetting.Profile)
- m.Post("", bindIgnErr(forms.UpdateProfileForm{}), userSetting.ProfilePost)
- m.Get("/change_password", user.MustChangePassword)
- m.Post("/change_password", bindIgnErr(forms.MustChangePasswordForm{}), user.MustChangePasswordPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), userSetting.AvatarPost)
- m.Post("/avatar/delete", userSetting.DeleteAvatar)
- m.Group("/account", func() {
- m.Combo("").Get(userSetting.Account).Post(bindIgnErr(forms.ChangePasswordForm{}), userSetting.AccountPost)
- m.Post("/email", bindIgnErr(forms.AddEmailForm{}), userSetting.EmailPost)
- m.Post("/email/delete", userSetting.DeleteEmail)
- m.Post("/delete", userSetting.DeleteAccount)
- m.Post("/theme", bindIgnErr(forms.UpdateThemeForm{}), userSetting.UpdateUIThemePost)
- })
- m.Group("/security", func() {
- m.Get("", userSetting.Security)
- m.Group("/two_factor", func() {
- m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
- m.Post("/disable", userSetting.DisableTwoFactor)
- m.Get("/enroll", userSetting.EnrollTwoFactor)
- m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
- })
- m.Group("/u2f", func() {
- m.Post("/request_register", bindIgnErr(forms.U2FRegistrationForm{}), userSetting.U2FRegister)
- m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
- m.Post("/delete", bindIgnErr(forms.U2FDeleteForm{}), userSetting.U2FDelete)
- })
- m.Group("/openid", func() {
- m.Post("", bindIgnErr(forms.AddOpenIDForm{}), userSetting.OpenIDPost)
- m.Post("/delete", userSetting.DeleteOpenID)
- m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
- }, openIDSignInEnabled)
- m.Post("/account_link", userSetting.DeleteAccountLink)
- })
- m.Group("/applications/oauth2", func() {
- m.Get("/{id}", userSetting.OAuth2ApplicationShow)
- m.Post("/{id}", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
- m.Post("/{id}/regenerate_secret", userSetting.OAuthApplicationsRegenerateSecret)
- m.Post("", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
- m.Post("/delete", userSetting.DeleteOAuth2Application)
- m.Post("/revoke", userSetting.RevokeOAuth2Grant)
- })
- m.Combo("/applications").Get(userSetting.Applications).
- Post(bindIgnErr(forms.NewAccessTokenForm{}), userSetting.ApplicationsPost)
- m.Post("/applications/delete", userSetting.DeleteApplication)
- m.Combo("/keys").Get(userSetting.Keys).
- Post(bindIgnErr(forms.AddKeyForm{}), userSetting.KeysPost)
- m.Post("/keys/delete", userSetting.DeleteKey)
- m.Get("/organization", userSetting.Organization)
- m.Get("/repos", userSetting.Repos)
- m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository)
- }, reqSignIn, func(ctx *context.Context) {
- ctx.Data["PageIsUserSettings"] = true
- ctx.Data["AllThemes"] = setting.UI.Themes
- })
-
- m.Group("/user", func() {
- // r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
- m.Get("/activate", user.Activate, reqSignIn)
- m.Post("/activate", user.ActivatePost, reqSignIn)
- m.Any("/activate_email", user.ActivateEmail)
- m.Get("/avatar/{username}/{size}", user.Avatar)
- m.Get("/email2user", user.Email2User)
- m.Get("/recover_account", user.ResetPasswd)
- m.Post("/recover_account", user.ResetPasswdPost)
- m.Get("/forgot_password", user.ForgotPasswd)
- m.Post("/forgot_password", user.ForgotPasswdPost)
- m.Post("/logout", user.SignOut)
- m.Get("/task/{task}", user.TaskStatus)
- })
- // ***** END: User *****
-
- m.Get("/avatar/{hash}", user.AvatarByEmailHash)
-
- adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
-
- // ***** START: Admin *****
- m.Group("/admin", func() {
- m.Get("", adminReq, admin.Dashboard)
- m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost)
- m.Get("/config", admin.Config)
- m.Post("/config/test_mail", admin.SendTestMail)
- m.Group("/monitor", func() {
- m.Get("", admin.Monitor)
- m.Post("/cancel/{pid}", admin.MonitorCancel)
- m.Group("/queue/{qid}", func() {
- m.Get("", admin.Queue)
- m.Post("/set", admin.SetQueueSettings)
- m.Post("/add", admin.AddWorkers)
- m.Post("/cancel/{pid}", admin.WorkerCancel)
- m.Post("/flush", admin.Flush)
- })
- })
-
- m.Group("/users", func() {
- m.Get("", admin.Users)
- m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(forms.AdminCreateUserForm{}), admin.NewUserPost)
- m.Combo("/{userid}").Get(admin.EditUser).Post(bindIgnErr(forms.AdminEditUserForm{}), admin.EditUserPost)
- m.Post("/{userid}/delete", admin.DeleteUser)
- })
-
- m.Group("/emails", func() {
- m.Get("", admin.Emails)
- m.Post("/activate", admin.ActivateEmail)
- })
-
- m.Group("/orgs", func() {
- m.Get("", admin.Organizations)
- })
-
- m.Group("/repos", func() {
- m.Get("", admin.Repos)
- m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
- m.Post("/delete", admin.DeleteRepo)
- })
-
- m.Group("/hooks", func() {
- m.Get("", admin.DefaultOrSystemWebhooks)
- m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
- m.Get("/{id}", repo.WebHooksEdit)
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- }, webhooksEnabled)
-
- m.Group("/{configType:default-hooks|system-hooks}", func() {
- m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- })
-
- m.Group("/auths", func() {
- m.Get("", admin.Authentications)
- m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
- m.Combo("/{authid}").Get(admin.EditAuthSource).
- Post(bindIgnErr(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
- m.Post("/{authid}/delete", admin.DeleteAuthSource)
- })
-
- m.Group("/notices", func() {
- m.Get("", admin.Notices)
- m.Post("/delete", admin.DeleteNotices)
- m.Post("/empty", admin.EmptyNotices)
- })
- }, adminReq)
- // ***** END: Admin *****
-
- m.Group("", func() {
- m.Get("/{username}", user.Profile)
- m.Get("/attachments/{uuid}", repo.GetAttachment)
- }, ignSignIn)
-
- m.Group("/{username}", func() {
- m.Post("/action/{action}", user.Action)
- }, reqSignIn)
-
- if !setting.IsProd() {
- m.Get("/template/*", dev.TemplatePreview)
- }
-
- reqRepoAdmin := context.RequireRepoAdmin()
- reqRepoCodeWriter := context.RequireRepoWriter(models.UnitTypeCode)
- reqRepoCodeReader := context.RequireRepoReader(models.UnitTypeCode)
- reqRepoReleaseWriter := context.RequireRepoWriter(models.UnitTypeReleases)
- reqRepoReleaseReader := context.RequireRepoReader(models.UnitTypeReleases)
- reqRepoWikiWriter := context.RequireRepoWriter(models.UnitTypeWiki)
- reqRepoIssueWriter := context.RequireRepoWriter(models.UnitTypeIssues)
- reqRepoIssueReader := context.RequireRepoReader(models.UnitTypeIssues)
- reqRepoPullsReader := context.RequireRepoReader(models.UnitTypePullRequests)
- reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
- reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
- reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects)
- reqRepoProjectsWriter := context.RequireRepoWriter(models.UnitTypeProjects)
-
- // ***** START: Organization *****
- m.Group("/org", func() {
- m.Group("", func() {
- m.Get("/create", org.Create)
- m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
- })
-
- m.Group("/{org}", func() {
- m.Get("/dashboard", user.Dashboard)
- m.Get("/dashboard/{team}", user.Dashboard)
- m.Get("/issues", user.Issues)
- m.Get("/issues/{team}", user.Issues)
- m.Get("/pulls", user.Pulls)
- m.Get("/pulls/{team}", user.Pulls)
- m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
- m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
- m.Get("/members", org.Members)
- m.Post("/members/action/{action}", org.MembersAction)
- m.Get("/teams", org.Teams)
- }, context.OrgAssignment(true, false, true))
-
- m.Group("/{org}", func() {
- m.Get("/teams/{team}", org.TeamMembers)
- m.Get("/teams/{team}/repositories", org.TeamRepositories)
- m.Post("/teams/{team}/action/{action}", org.TeamsAction)
- m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
- }, context.OrgAssignment(true, false, true))
-
- m.Group("/{org}", func() {
- m.Get("/teams/new", org.NewTeam)
- m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
- m.Get("/teams/{team}/edit", org.EditTeam)
- m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
- m.Post("/teams/{team}/delete", org.DeleteTeam)
-
- m.Group("/settings", func() {
- m.Combo("").Get(org.Settings).
- Post(bindIgnErr(forms.UpdateOrgSettingForm{}), org.SettingsPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), org.SettingsAvatar)
- m.Post("/avatar/delete", org.SettingsDeleteAvatar)
-
- m.Group("/hooks", func() {
- m.Get("", org.Webhooks)
- m.Post("/delete", org.DeleteWebhook)
- m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- m.Get("/{id}", repo.WebHooksEdit)
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- }, webhooksEnabled)
-
- m.Group("/labels", func() {
- m.Get("", org.RetrieveLabels, org.Labels)
- m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), org.NewLabel)
- m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), org.UpdateLabel)
- m.Post("/delete", org.DeleteLabel)
- m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), org.InitializeLabels)
- })
-
- m.Route("/delete", "GET,POST", org.SettingsDelete)
- })
- }, context.OrgAssignment(true, true))
- }, reqSignIn)
- // ***** END: Organization *****
-
- // ***** START: Repository *****
- m.Group("/repo", func() {
- m.Get("/create", repo.Create)
- m.Post("/create", bindIgnErr(forms.CreateRepoForm{}), repo.CreatePost)
- m.Get("/migrate", repo.Migrate)
- m.Post("/migrate", bindIgnErr(forms.MigrateRepoForm{}), repo.MigratePost)
- m.Group("/fork", func() {
- m.Combo("/{repoid}").Get(repo.Fork).
- Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
- }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
- }, reqSignIn)
-
- // ***** Release Attachment Download without Signin
- m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
-
- m.Group("/{username}/{reponame}", func() {
- m.Group("/settings", func() {
- m.Combo("").Get(repo.Settings).
- Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar)
- m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
-
- m.Group("/collaboration", func() {
- m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
- m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
- m.Post("/delete", repo.DeleteCollaboration)
- m.Group("/team", func() {
- m.Post("", repo.AddTeamPost)
- m.Post("/delete", repo.DeleteTeam)
- })
- })
- m.Group("/branches", func() {
- m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
- m.Combo("/*").Get(repo.SettingsProtectedBranch).
- Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
- }, repo.MustBeNotEmpty)
-
- m.Group("/hooks/git", func() {
- m.Get("", repo.GitHooks)
- m.Combo("/{name}").Get(repo.GitHooksEdit).
- Post(repo.GitHooksEditPost)
- }, context.GitHookService())
-
- m.Group("/hooks", func() {
- m.Get("", repo.Webhooks)
- m.Post("/delete", repo.DeleteWebhook)
- m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- m.Get("/{id}", repo.WebHooksEdit)
- m.Post("/{id}/test", repo.TestWebhook)
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- }, webhooksEnabled)
-
- m.Group("/keys", func() {
- m.Combo("").Get(repo.DeployKeys).
- Post(bindIgnErr(forms.AddKeyForm{}), repo.DeployKeysPost)
- m.Post("/delete", repo.DeleteDeployKey)
- })
-
- m.Group("/lfs", func() {
- m.Get("/", repo.LFSFiles)
- m.Get("/show/{oid}", repo.LFSFileGet)
- m.Post("/delete/{oid}", repo.LFSDelete)
- m.Get("/pointers", repo.LFSPointerFiles)
- m.Post("/pointers/associate", repo.LFSAutoAssociate)
- m.Get("/find", repo.LFSFileFind)
- m.Group("/locks", func() {
- m.Get("/", repo.LFSLocks)
- m.Post("/", repo.LFSLockFile)
- m.Post("/{lid}/unlock", repo.LFSUnlock)
- })
- })
-
- }, func(ctx *context.Context) {
- ctx.Data["PageIsSettings"] = true
- ctx.Data["LFSStartServer"] = setting.LFS.StartServer
- })
- }, reqSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoAdmin, context.RepoRef())
-
- m.Post("/{username}/{reponame}/action/{action}", reqSignIn, context.RepoAssignment, context.UnitTypes(), repo.Action)
-
- // Grouping for those endpoints not requiring authentication
- m.Group("/{username}/{reponame}", func() {
- m.Group("/milestone", func() {
- m.Get("/{id}", repo.MilestoneIssuesAndPulls)
- }, reqRepoIssuesOrPullsReader, context.RepoRef())
- m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
- Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
- Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
- }, context.RepoAssignment, context.UnitTypes())
-
- // Grouping for those endpoints that do require authentication
- m.Group("/{username}/{reponame}", func() {
- m.Group("/issues", func() {
- m.Group("/new", func() {
- m.Combo("").Get(context.RepoRef(), repo.NewIssue).
- Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
- m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
- })
- }, context.RepoMustNotBeArchived(), reqRepoIssueReader)
- // FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
- // So they can apply their own enable/disable logic on routers.
- m.Group("/issues", func() {
- m.Group("/{index}", func() {
- m.Post("/title", repo.UpdateIssueTitle)
- m.Post("/content", repo.UpdateIssueContent)
- m.Post("/watch", repo.IssueWatch)
- m.Post("/ref", repo.UpdateIssueRef)
- m.Group("/dependency", func() {
- m.Post("/add", repo.AddDependency)
- m.Post("/delete", repo.RemoveDependency)
- })
- m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
- m.Group("/times", func() {
- m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
- m.Post("/{timeid}/delete", repo.DeleteTime)
- m.Group("/stopwatch", func() {
- m.Post("/toggle", repo.IssueStopwatch)
- m.Post("/cancel", repo.CancelStopwatch)
- })
- })
- m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction)
- m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue)
- m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
- }, context.RepoMustNotBeArchived())
- m.Group("/{index}", func() {
- m.Get("/attachments", repo.GetIssueAttachments)
- m.Get("/attachments/{uuid}", repo.GetAttachment)
- })
-
- m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
- m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
- m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
- m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
- m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
- m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(forms.DismissReviewForm{}), repo.DismissReview)
- m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
- m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
- m.Post("/attachments", repo.UploadIssueAttachment)
- m.Post("/attachments/remove", repo.DeleteAttachment)
- }, context.RepoMustNotBeArchived())
- m.Group("/comments/{id}", func() {
- m.Post("", repo.UpdateCommentContent)
- m.Post("/delete", repo.DeleteComment)
- m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeCommentReaction)
- }, context.RepoMustNotBeArchived())
- m.Group("/comments/{id}", func() {
- m.Get("/attachments", repo.GetCommentAttachments)
- })
- m.Group("/labels", func() {
- m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel)
- m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel)
- m.Post("/delete", repo.DeleteLabel)
- m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), repo.InitializeLabels)
- }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
- m.Group("/milestones", func() {
- m.Combo("/new").Get(repo.NewMilestone).
- Post(bindIgnErr(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
- m.Get("/{id}/edit", repo.EditMilestone)
- m.Post("/{id}/edit", bindIgnErr(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
- m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
- m.Post("/delete", repo.DeleteMilestone)
- }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
- m.Group("/pull", func() {
- m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget)
- }, context.RepoMustNotBeArchived())
-
- m.Group("", func() {
- m.Group("", func() {
- m.Combo("/_edit/*").Get(repo.EditFile).
- Post(bindIgnErr(forms.EditRepoFileForm{}), repo.EditFilePost)
- m.Combo("/_new/*").Get(repo.NewFile).
- Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewFilePost)
- m.Post("/_preview/*", bindIgnErr(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
- m.Combo("/_delete/*").Get(repo.DeleteFile).
- Post(bindIgnErr(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
- m.Combo("/_upload/*", repo.MustBeAbleToUpload).
- Get(repo.UploadFile).
- Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
- }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
- m.Group("", func() {
- m.Post("/upload-file", repo.UploadFileToServer)
- m.Post("/upload-remove", bindIgnErr(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
- }, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
- }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
-
- m.Group("/branches", func() {
- m.Group("/_new", func() {
- m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
- m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
- m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
- }, bindIgnErr(forms.NewBranchForm{}))
- m.Post("/delete", repo.DeleteBranchPost)
- m.Post("/restore", repo.RestoreBranchPost)
- }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
-
- }, reqSignIn, context.RepoAssignment, context.UnitTypes())
-
- // Releases
- m.Group("/{username}/{reponame}", func() {
- m.Get("/tags", repo.TagsList, repo.MustBeNotEmpty,
- reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag))
- m.Group("/releases", func() {
- m.Get("/", repo.Releases)
- m.Get("/tag/*", repo.SingleRelease)
- m.Get("/latest", repo.LatestRelease)
- }, repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
- m.Get("/releases/attachments/{uuid}", repo.GetAttachment, repo.MustBeNotEmpty, reqRepoReleaseReader)
- m.Group("/releases", func() {
- m.Get("/new", repo.NewRelease)
- m.Post("/new", bindIgnErr(forms.NewReleaseForm{}), repo.NewReleasePost)
- m.Post("/delete", repo.DeleteRelease)
- m.Post("/attachments", repo.UploadReleaseAttachment)
- m.Post("/attachments/remove", repo.DeleteAttachment)
- }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
- m.Post("/tags/delete", repo.DeleteTag, reqSignIn,
- repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
- m.Group("/releases", func() {
- m.Get("/edit/*", repo.EditRelease)
- m.Post("/edit/*", bindIgnErr(forms.EditReleaseForm{}), repo.EditReleasePost)
- }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) {
- var err error
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
- ctx.ServerError("GetBranchCommit", err)
- return
- }
- ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
- if err != nil {
- ctx.ServerError("GetCommitsCount", err)
- return
- }
- ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
- })
- m.Get("/attachments/{uuid}", repo.GetAttachment)
- }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
-
- m.Group("/{username}/{reponame}", func() {
- m.Post("/topics", repo.TopicsPost)
- }, context.RepoAssignment, context.RepoMustNotBeArchived(), reqRepoAdmin)
-
- m.Group("/{username}/{reponame}", func() {
- m.Group("", func() {
- m.Get("/{type:issues|pulls}", repo.Issues)
- m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
- m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
- m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
- }, context.RepoRef())
-
- m.Group("/projects", func() {
- m.Get("", repo.Projects)
- m.Get("/{id}", repo.ViewProject)
- m.Group("", func() {
- m.Get("/new", repo.NewProject)
- m.Post("/new", bindIgnErr(forms.CreateProjectForm{}), repo.NewProjectPost)
- m.Group("/{id}", func() {
- m.Post("", bindIgnErr(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
- m.Post("/delete", repo.DeleteProject)
-
- m.Get("/edit", repo.EditProject)
- m.Post("/edit", bindIgnErr(forms.CreateProjectForm{}), repo.EditProjectPost)
- m.Post("/{action:open|close}", repo.ChangeProjectStatus)
-
- m.Group("/{boardID}", func() {
- m.Put("", bindIgnErr(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
- m.Delete("", repo.DeleteProjectBoard)
- m.Post("/default", repo.SetDefaultProjectBoard)
-
- m.Post("/{index}", repo.MoveIssueAcrossBoards)
- })
- })
- }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
- }, reqRepoProjectsReader, repo.MustEnableProjects)
-
- m.Group("/wiki", func() {
- m.Get("/", repo.Wiki)
- m.Get("/{page}", repo.Wiki)
- m.Get("/_pages", repo.WikiPages)
- m.Get("/{page}/_revision", repo.WikiRevision)
- m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
- m.Get("/commit/{sha:[a-f0-9]{7,40}}.{:patch|diff}", repo.RawDiff)
-
- m.Group("", func() {
- m.Combo("/_new").Get(repo.NewWiki).
- Post(bindIgnErr(forms.NewWikiForm{}), repo.NewWikiPost)
- m.Combo("/{page}/_edit").Get(repo.EditWiki).
- Post(bindIgnErr(forms.NewWikiForm{}), repo.EditWikiPost)
- m.Post("/{page}/delete", repo.DeleteWikiPagePost)
- }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter)
- }, repo.MustEnableWiki, context.RepoRef(), func(ctx *context.Context) {
- ctx.Data["PageIsWiki"] = true
- })
-
- m.Group("/wiki", func() {
- m.Get("/raw/*", repo.WikiRaw)
- }, repo.MustEnableWiki)
-
- m.Group("/activity", func() {
- m.Get("", repo.Activity)
- m.Get("/{period}", repo.Activity)
- }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))
-
- m.Group("/activity_author_data", func() {
- m.Get("", repo.ActivityAuthors)
- m.Get("/{period}", repo.ActivityAuthors)
- }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
-
- m.Group("/archive", func() {
- m.Get("/*", repo.Download)
- m.Post("/*", repo.InitiateDownload)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
- m.Group("/branches", func() {
- m.Get("", repo.Branches)
- }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
- m.Group("/blob_excerpt", func() {
- m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
- }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
- m.Group("/pulls/{index}", func() {
- m.Get(".diff", repo.DownloadPullDiff)
- m.Get(".patch", repo.DownloadPullPatch)
- m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
- m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
- m.Post("/update", repo.UpdatePullRequest)
- m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
- m.Group("/files", func() {
- m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
- m.Group("/reviews", func() {
- m.Get("/new_comment", repo.RenderNewCodeCommentForm)
- m.Post("/comments", bindIgnErr(forms.CodeCommentForm{}), repo.CreateCodeComment)
- m.Post("/submit", bindIgnErr(forms.SubmitReviewForm{}), repo.SubmitReview)
- }, context.RepoMustNotBeArchived())
- })
- }, repo.MustAllowPulls)
-
- m.Group("/media", func() {
- m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
- m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)
- m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS)
- m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS)
- // "/*" route is deprecated, and kept for backward compatibility
- m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownloadOrLFS)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
- m.Group("/raw", func() {
- m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
- m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
- m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
- m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID)
- // "/*" route is deprecated, and kept for backward compatibility
- m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
- m.Group("/commits", func() {
- m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
- m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
- m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits)
- // "/*" route is deprecated, and kept for backward compatibility
- m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
- m.Group("/blame", func() {
- m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame)
- m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame)
- m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
-
- m.Group("", func() {
- m.Get("/graph", repo.Graph)
- m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
- }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
-
- m.Group("/src", func() {
- m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
- m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
- m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
- // "/*" route is deprecated, and kept for backward compatibility
- m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
- }, repo.SetEditorconfigIfExists)
-
- m.Group("", func() {
- m.Get("/forks", repo.Forks)
- }, context.RepoRef(), reqRepoCodeReader)
- m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}",
- repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
- }, ignSignIn, context.RepoAssignment, context.UnitTypes())
- m.Group("/{username}/{reponame}", func() {
- m.Get("/stars", repo.Stars)
- m.Get("/watchers", repo.Watchers)
- m.Get("/search", reqRepoCodeReader, repo.Search)
- }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
-
- m.Group("/{username}", func() {
- m.Group("/{reponame}", func() {
- m.Get("", repo.SetEditorconfigIfExists, repo.Home)
- }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
-
- m.Group("/{reponame}", func() {
- m.Group("/info/lfs", func() {
- m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
- m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
- m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
- m.Get("/objects/{oid}", lfs.DownloadHandler)
- m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
- m.Group("/locks", func() {
- m.Get("/", lfs.GetListLockHandler)
- m.Post("/", lfs.PostLockHandler)
- m.Post("/verify", lfs.VerifyLockHandler)
- m.Post("/{lid}/unlock", lfs.UnLockHandler)
- }, lfs.CheckAcceptMediaType)
- m.Any("/*", func(ctx *context.Context) {
- ctx.NotFound("", nil)
- })
- }, ignSignInAndCsrf, lfsServerEnabled)
-
- m.Group("", func() {
- m.Post("/git-upload-pack", repo.ServiceUploadPack)
- m.Post("/git-receive-pack", repo.ServiceReceivePack)
- m.Get("/info/refs", repo.GetInfoRefs)
- m.Get("/HEAD", repo.GetTextFile("HEAD"))
- m.Get("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
- m.Get("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
- m.Get("/objects/info/packs", repo.GetInfoPacks)
- m.Get("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
- m.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
- m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
- m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
- }, ignSignInAndCsrf)
-
- m.Head("/tasks/trigger", repo.TriggerTask)
- })
- })
- // ***** END: Repository *****
-
- m.Group("/notifications", func() {
- m.Get("", user.Notifications)
- m.Post("/status", user.NotificationStatusPost)
- m.Post("/purge", user.NotificationPurgePost)
- }, reqSignIn)
-
- if setting.API.EnableSwagger {
- m.Get("/swagger.v1.json", routers.SwaggerV1Json)
- }
-
- // Not found handler.
- m.NotFound(web.Wrap(routers.NotFound))
-}
+++ /dev/null
-// 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 routers
-
-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)
- }
-}
+++ /dev/null
-// 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 user
-
-import (
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "strings"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/modules/auth/oauth2"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/eventsource"
- "code.gitea.io/gitea/modules/hcaptcha"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/password"
- "code.gitea.io/gitea/modules/recaptcha"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/utils"
- "code.gitea.io/gitea/services/externalaccount"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/mailer"
-
- "github.com/markbates/goth"
- "github.com/tstranex/u2f"
-)
-
-const (
- // tplMustChangePassword template for updating a user's password
- tplMustChangePassword = "user/auth/change_passwd"
- // tplSignIn template for sign in page
- tplSignIn base.TplName = "user/auth/signin"
- // tplSignUp template path for sign up page
- tplSignUp base.TplName = "user/auth/signup"
- // TplActivate template path for activate user
- TplActivate base.TplName = "user/auth/activate"
- tplForgotPassword base.TplName = "user/auth/forgot_passwd"
- tplResetPassword base.TplName = "user/auth/reset_passwd"
- tplTwofa base.TplName = "user/auth/twofa"
- tplTwofaScratch base.TplName = "user/auth/twofa_scratch"
- tplLinkAccount base.TplName = "user/auth/link_account"
- tplU2F base.TplName = "user/auth/u2f"
-)
-
-// AutoSignIn reads cookie and try to auto-login.
-func AutoSignIn(ctx *context.Context) (bool, error) {
- if !models.HasEngine {
- return false, nil
- }
-
- uname := ctx.GetCookie(setting.CookieUserName)
- if len(uname) == 0 {
- return false, nil
- }
-
- isSucceed := false
- defer func() {
- if !isSucceed {
- log.Trace("auto-login cookie cleared: %s", uname)
- ctx.DeleteCookie(setting.CookieUserName)
- ctx.DeleteCookie(setting.CookieRememberName)
- }
- }()
-
- u, err := models.GetUserByName(uname)
- if err != nil {
- if !models.IsErrUserNotExist(err) {
- return false, fmt.Errorf("GetUserByName: %v", err)
- }
- return false, nil
- }
-
- if val, ok := ctx.GetSuperSecureCookie(
- base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
- return false, nil
- }
-
- isSucceed = true
-
- // Set session IDs
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- return false, err
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- return false, err
- }
- if err := ctx.Session.Release(); err != nil {
- return false, err
- }
-
- middleware.DeleteCSRFCookie(ctx.Resp)
- return true, nil
-}
-
-func checkAutoLogin(ctx *context.Context) bool {
- // Check auto-login.
- isSucceed, err := AutoSignIn(ctx)
- if err != nil {
- ctx.ServerError("AutoSignIn", err)
- return true
- }
-
- 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, setting.AppSubURL+string(setting.LandingPageURL))
- return true
- }
-
- return false
-}
-
-// SignIn render sign in page
-func SignIn(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("sign_in")
-
- // Check auto-login.
- if checkAutoLogin(ctx) {
- return
- }
-
- orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
- ctx.Data["OAuth2Providers"] = oauth2Providers
- ctx.Data["Title"] = ctx.Tr("sign_in")
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
- ctx.Data["PageIsSignIn"] = true
- ctx.Data["PageIsLogin"] = true
- ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
-
- ctx.HTML(http.StatusOK, tplSignIn)
-}
-
-// SignInPost response for sign in request
-func SignInPost(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("sign_in")
-
- orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
- ctx.Data["OAuth2Providers"] = oauth2Providers
- ctx.Data["Title"] = ctx.Tr("sign_in")
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
- ctx.Data["PageIsSignIn"] = true
- ctx.Data["PageIsLogin"] = true
- ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplSignIn)
- return
- }
-
- form := web.GetForm(ctx).(*forms.SignInForm)
- u, err := models.UserSignIn(form.UserName, form.Password)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
- log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
- } else if models.IsErrEmailAlreadyUsed(err) {
- ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
- log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
- } else if models.IsErrUserProhibitLogin(err) {
- log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
- ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
- ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
- } else if models.IsErrUserInactive(err) {
- if setting.Service.RegisterEmailConfirm {
- ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
- ctx.HTML(http.StatusOK, TplActivate)
- } else {
- log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
- ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
- ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
- }
- } else {
- ctx.ServerError("UserSignIn", err)
- }
- return
- }
- // If this user is enrolled in 2FA, we can't sign the user in just yet.
- // Instead, redirect them to the 2FA authentication page.
- _, err = models.GetTwoFactorByUID(u.ID)
- if err != nil {
- if models.IsErrTwoFactorNotEnrolled(err) {
- handleSignIn(ctx, u, form.Remember)
- } else {
- ctx.ServerError("UserSignIn", err)
- }
- return
- }
-
- // User needs to use 2FA, save data and redirect to 2FA page.
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
- return
- }
- if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
- ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
- return
- }
- if err := ctx.Session.Release(); err != nil {
- ctx.ServerError("UserSignIn: Unable to save session", err)
- return
- }
-
- regs, err := models.GetU2FRegistrationsByUID(u.ID)
- if err == nil && len(regs) > 0 {
- ctx.Redirect(setting.AppSubURL + "/user/u2f")
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// TwoFactor shows the user a two-factor authentication page.
-func TwoFactor(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("twofa")
-
- // Check auto-login.
- if checkAutoLogin(ctx) {
- return
- }
-
- // Ensure user is in a 2FA session.
- if ctx.Session.Get("twofaUid") == nil {
- ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
- return
- }
-
- ctx.HTML(http.StatusOK, tplTwofa)
-}
-
-// TwoFactorPost validates a user's two-factor authentication token.
-func TwoFactorPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
- ctx.Data["Title"] = ctx.Tr("twofa")
-
- // Ensure user is in a 2FA session.
- idSess := ctx.Session.Get("twofaUid")
- if idSess == nil {
- ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
- return
- }
-
- id := idSess.(int64)
- twofa, err := models.GetTwoFactorByUID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- // Validate the passcode with the stored TOTP secret.
- ok, err := twofa.ValidateTOTP(form.Passcode)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if ok && twofa.LastUsedPasscode != form.Passcode {
- remember := ctx.Session.Get("twofaRemember").(bool)
- u, err := models.GetUserByID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if ctx.Session.Get("linkAccount") != nil {
- gothUser := ctx.Session.Get("linkAccountGothUser")
- if gothUser == nil {
- ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
- return
- }
-
- err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- }
-
- twofa.LastUsedPasscode = form.Passcode
- if err = models.UpdateTwoFactor(twofa); err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- handleSignIn(ctx, u, remember)
- return
- }
-
- ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
-}
-
-// TwoFactorScratch shows the scratch code form for two-factor authentication.
-func TwoFactorScratch(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("twofa_scratch")
-
- // Check auto-login.
- if checkAutoLogin(ctx) {
- return
- }
-
- // Ensure user is in a 2FA session.
- if ctx.Session.Get("twofaUid") == nil {
- ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
- return
- }
-
- ctx.HTML(http.StatusOK, tplTwofaScratch)
-}
-
-// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
-func TwoFactorScratchPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.TwoFactorScratchAuthForm)
- ctx.Data["Title"] = ctx.Tr("twofa_scratch")
-
- // Ensure user is in a 2FA session.
- idSess := ctx.Session.Get("twofaUid")
- if idSess == nil {
- ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
- return
- }
-
- id := idSess.(int64)
- twofa, err := models.GetTwoFactorByUID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- // Validate the passcode with the stored TOTP secret.
- if twofa.VerifyScratchToken(form.Token) {
- // Invalidate the scratch token.
- _, err = twofa.GenerateScratchToken()
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if err = models.UpdateTwoFactor(twofa); err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- remember := ctx.Session.Get("twofaRemember").(bool)
- u, err := models.GetUserByID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- handleSignInFull(ctx, u, remember, false)
- ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
- return
- }
-
- ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
-}
-
-// U2F shows the U2F login page
-func U2F(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("twofa")
- ctx.Data["RequireU2F"] = true
- // Check auto-login.
- if checkAutoLogin(ctx) {
- return
- }
-
- // Ensure user is in a 2FA session.
- if ctx.Session.Get("twofaUid") == nil {
- ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
- return
- }
-
- ctx.HTML(http.StatusOK, tplU2F)
-}
-
-// U2FChallenge submits a sign challenge to the browser
-func U2FChallenge(ctx *context.Context) {
- // Ensure user is in a U2F session.
- idSess := ctx.Session.Get("twofaUid")
- if idSess == nil {
- ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
- return
- }
- id := idSess.(int64)
- regs, err := models.GetU2FRegistrationsByUID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if len(regs) == 0 {
- ctx.ServerError("UserSignIn", errors.New("no device registered"))
- return
- }
- challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
- if err != nil {
- ctx.ServerError("u2f.NewChallenge", err)
- return
- }
- if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
- ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
- return
- }
- if err := ctx.Session.Release(); err != nil {
- ctx.ServerError("UserSignIn: unable to store session", err)
- }
-
- ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations()))
-}
-
-// U2FSign authenticates the user by signResp
-func U2FSign(ctx *context.Context) {
- signResp := web.GetForm(ctx).(*u2f.SignResponse)
- challSess := ctx.Session.Get("u2fChallenge")
- idSess := ctx.Session.Get("twofaUid")
- if challSess == nil || idSess == nil {
- ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
- return
- }
- challenge := challSess.(*u2f.Challenge)
- id := idSess.(int64)
- regs, err := models.GetU2FRegistrationsByUID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- for _, reg := range regs {
- r, err := reg.Parse()
- if err != nil {
- log.Fatal("parsing u2f registration: %v", err)
- continue
- }
- newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
- if authErr == nil {
- reg.Counter = newCounter
- user, err := models.GetUserByID(id)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- remember := ctx.Session.Get("twofaRemember").(bool)
- if err := reg.UpdateCounter(); err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if ctx.Session.Get("linkAccount") != nil {
- gothUser := ctx.Session.Get("linkAccountGothUser")
- if gothUser == nil {
- ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
- return
- }
-
- err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User))
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- }
- redirect := handleSignInFull(ctx, user, remember, false)
- if redirect == "" {
- redirect = setting.AppSubURL + "/"
- }
- ctx.PlainText(200, []byte(redirect))
- return
- }
- }
- ctx.Error(http.StatusUnauthorized)
-}
-
-// This handles the final part of the sign-in process of the user.
-func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
- handleSignInFull(ctx, u, remember, true)
-}
-
-func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
- if remember {
- days := 86400 * setting.LogInRememberDays
- ctx.SetCookie(setting.CookieUserName, u.Name, days)
- ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
- setting.CookieRememberName, u.Name, days)
- }
-
- _ = ctx.Session.Delete("openid_verified_uri")
- _ = ctx.Session.Delete("openid_signin_remember")
- _ = ctx.Session.Delete("openid_determined_email")
- _ = ctx.Session.Delete("openid_determined_username")
- _ = ctx.Session.Delete("twofaUid")
- _ = ctx.Session.Delete("twofaRemember")
- _ = ctx.Session.Delete("u2fChallenge")
- _ = ctx.Session.Delete("linkAccount")
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- log.Error("Error setting uid %d in session: %v", u.ID, err)
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- log.Error("Error setting uname %s session: %v", u.Name, err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Unable to store session: %v", err)
- }
-
- // Language setting of the user overwrites the one previously set
- // If the user does not have a locale set, we save the current one.
- if len(u.Language) == 0 {
- u.Language = ctx.Locale.Language()
- if err := models.UpdateUserCols(u, "language"); err != nil {
- log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
- return setting.AppSubURL + "/"
- }
- }
-
- middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
-
- // Clear whatever CSRF has right now, force to generate a new one
- middleware.DeleteCSRFCookie(ctx.Resp)
-
- // Register last login
- u.SetLastLogin()
- if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
- return setting.AppSubURL + "/"
- }
-
- if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- if obeyRedirect {
- ctx.RedirectToFirst(redirectTo)
- }
- return redirectTo
- }
-
- if obeyRedirect {
- ctx.Redirect(setting.AppSubURL + "/")
- }
- return setting.AppSubURL + "/"
-}
-
-// SignInOAuth handles the OAuth2 login buttons
-func SignInOAuth(ctx *context.Context) {
- provider := ctx.Params(":provider")
-
- loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
- if err != nil {
- ctx.ServerError("SignIn", err)
- return
- }
-
- // try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
- user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
- if err == nil && user != nil {
- // we got the user without going through the whole OAuth2 authentication flow again
- handleOAuth2SignIn(ctx, user, gothUser)
- return
- }
-
- if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
- if strings.Contains(err.Error(), "no provider for ") {
- if err = models.ResetOAuth2(); err != nil {
- ctx.ServerError("SignIn", err)
- return
- }
- if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
- ctx.ServerError("SignIn", err)
- }
- return
- }
- ctx.ServerError("SignIn", err)
- }
- // redirect is done in oauth2.Auth
-}
-
-// SignInOAuthCallback handles the callback from the given provider
-func SignInOAuthCallback(ctx *context.Context) {
- provider := ctx.Params(":provider")
-
- // first look if the provider is still active
- loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
- if err != nil {
- ctx.ServerError("SignIn", err)
- return
- }
-
- if loginSource == nil {
- ctx.ServerError("SignIn", errors.New("No valid provider found, check configured callback url in provider"))
- return
- }
-
- u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
-
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if u == nil {
- if !(setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration) && setting.OAuth2Client.EnableAutoRegistration {
- // create new user with details from oauth2 provider
- var missingFields []string
- if gothUser.UserID == "" {
- missingFields = append(missingFields, "sub")
- }
- if gothUser.Email == "" {
- missingFields = append(missingFields, "email")
- }
- if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
- missingFields = append(missingFields, "nickname")
- }
- if len(missingFields) > 0 {
- log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
- if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
- log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
- }
- err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
- ctx.ServerError("CreateUser", err)
- return
- }
- u = &models.User{
- Name: getUserName(&gothUser),
- FullName: gothUser.Name,
- Email: gothUser.Email,
- IsActive: !setting.OAuth2Client.RegisterEmailConfirm,
- LoginType: models.LoginOAuth2,
- LoginSource: loginSource.ID,
- LoginName: gothUser.UserID,
- }
-
- if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
- // error already handled
- return
- }
- } else {
- // no existing user is found, request attach or new account
- showLinkingLogin(ctx, gothUser)
- return
- }
- }
-
- handleOAuth2SignIn(ctx, u, gothUser)
-}
-
-func getUserName(gothUser *goth.User) string {
- switch setting.OAuth2Client.Username {
- case setting.OAuth2UsernameEmail:
- return strings.Split(gothUser.Email, "@")[0]
- case setting.OAuth2UsernameNickname:
- return gothUser.NickName
- default: // OAuth2UsernameUserid
- return gothUser.UserID
- }
-}
-
-func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
- if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
- log.Error("Error setting linkAccountGothUser in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
- ctx.Redirect(setting.AppSubURL + "/user/link_account")
-}
-
-func updateAvatarIfNeed(url string, u *models.User) {
- if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
- resp, err := http.Get(url)
- if err == nil {
- defer func() {
- _ = resp.Body.Close()
- }()
- }
- // ignore any error
- if err == nil && resp.StatusCode == http.StatusOK {
- data, err := ioutil.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
- if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
- _ = u.UploadAvatar(data)
- }
- }
- }
-}
-
-func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User) {
- updateAvatarIfNeed(gothUser.AvatarURL, u)
-
- // If this user is enrolled in 2FA, we can't sign the user in just yet.
- // Instead, redirect them to the 2FA authentication page.
- _, err := models.GetTwoFactorByUID(u.ID)
- if err != nil {
- if !models.IsErrTwoFactorNotEnrolled(err) {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- log.Error("Error setting uid in session: %v", err)
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- log.Error("Error setting uname in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
- // Clear whatever CSRF has right now, force to generate a new one
- middleware.DeleteCSRFCookie(ctx.Resp)
-
- // Register last login
- u.SetLastLogin()
- if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUserCols", err)
- return
- }
-
- // update external user information
- if err := models.UpdateExternalUser(u, gothUser); err != nil {
- log.Error("UpdateExternalUser failed: %v", err)
- }
-
- if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo)
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/")
- return
- }
-
- // User needs to use 2FA, save data and redirect to 2FA page.
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- log.Error("Error setting twofaUid in session: %v", err)
- }
- if err := ctx.Session.Set("twofaRemember", false); err != nil {
- log.Error("Error setting twofaRemember in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
- // If U2F is enrolled -> Redirect to U2F instead
- regs, err := models.GetU2FRegistrationsByUID(u.ID)
- if err == nil && len(regs) > 0 {
- ctx.Redirect(setting.AppSubURL + "/user/u2f")
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
-// login the user
-func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
- gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
-
- if err != nil {
- if err.Error() == "securecookie: the value is too long" {
- log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
- err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
- }
- return nil, goth.User{}, err
- }
-
- user := &models.User{
- LoginName: gothUser.UserID,
- LoginType: models.LoginOAuth2,
- LoginSource: loginSource.ID,
- }
-
- hasUser, err := models.GetUser(user)
- if err != nil {
- return nil, goth.User{}, err
- }
-
- if hasUser {
- return user, gothUser, nil
- }
-
- // search in external linked users
- externalLoginUser := &models.ExternalLoginUser{
- ExternalID: gothUser.UserID,
- LoginSourceID: loginSource.ID,
- }
- hasUser, err = models.GetExternalLogin(externalLoginUser)
- if err != nil {
- return nil, goth.User{}, err
- }
- if hasUser {
- user, err = models.GetUserByID(externalLoginUser.UserID)
- return user, gothUser, err
- }
-
- // no user found to login
- return nil, gothUser, nil
-
-}
-
-// LinkAccount shows the page where the user can decide to login or create a new account
-func LinkAccount(ctx *context.Context) {
- ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
- ctx.Data["Title"] = ctx.Tr("link_account")
- ctx.Data["LinkAccountMode"] = true
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
- ctx.Data["Captcha"] = context.GetImageCaptcha()
- ctx.Data["CaptchaType"] = setting.Service.CaptchaType
- ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
- ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
- ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
- ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
- ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
- ctx.Data["ShowRegistrationButton"] = false
-
- // use this to set the right link into the signIn and signUp templates in the link_account template
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
- ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
- gothUser := ctx.Session.Get("linkAccountGothUser")
- if gothUser == nil {
- ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
- return
- }
-
- gu, _ := gothUser.(goth.User)
- uname := getUserName(&gu)
- email := gu.Email
- ctx.Data["user_name"] = uname
- ctx.Data["email"] = email
-
- if len(email) != 0 {
- u, err := models.GetUserByEmail(email)
- if err != nil && !models.IsErrUserNotExist(err) {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if u != nil {
- ctx.Data["user_exists"] = true
- }
- } else if len(uname) != 0 {
- u, err := models.GetUserByName(uname)
- if err != nil && !models.IsErrUserNotExist(err) {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if u != nil {
- ctx.Data["user_exists"] = true
- }
- }
-
- ctx.HTML(http.StatusOK, tplLinkAccount)
-}
-
-// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
-func LinkAccountPostSignIn(ctx *context.Context) {
- signInForm := web.GetForm(ctx).(*forms.SignInForm)
- ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
- ctx.Data["Title"] = ctx.Tr("link_account")
- ctx.Data["LinkAccountMode"] = true
- ctx.Data["LinkAccountModeSignIn"] = true
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
- 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["DisableRegistration"] = setting.Service.DisableRegistration
- ctx.Data["ShowRegistrationButton"] = false
-
- // use this to set the right link into the signIn and signUp templates in the link_account template
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
- ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
- gothUser := ctx.Session.Get("linkAccountGothUser")
- if gothUser == nil {
- ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplLinkAccount)
- return
- }
-
- u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.Data["user_exists"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
- } else {
- ctx.ServerError("UserLinkAccount", err)
- }
- return
- }
-
- linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
-}
-
-func linkAccount(ctx *context.Context, u *models.User, gothUser goth.User, remember bool) {
- updateAvatarIfNeed(gothUser.AvatarURL, u)
-
- // If this user is enrolled in 2FA, we can't sign the user in just yet.
- // Instead, redirect them to the 2FA authentication page.
- _, err := models.GetTwoFactorByUID(u.ID)
- if err != nil {
- if !models.IsErrTwoFactorNotEnrolled(err) {
- ctx.ServerError("UserLinkAccount", err)
- return
- }
-
- err = externalaccount.LinkAccountToUser(u, gothUser)
- if err != nil {
- ctx.ServerError("UserLinkAccount", err)
- return
- }
-
- handleSignIn(ctx, u, remember)
- return
- }
-
- // User needs to use 2FA, save data and redirect to 2FA page.
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- log.Error("Error setting twofaUid in session: %v", err)
- }
- if err := ctx.Session.Set("twofaRemember", remember); err != nil {
- log.Error("Error setting twofaRemember in session: %v", err)
- }
- if err := ctx.Session.Set("linkAccount", true); err != nil {
- log.Error("Error setting linkAccount in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
- // If U2F is enrolled -> Redirect to U2F instead
- regs, err := models.GetU2FRegistrationsByUID(u.ID)
- if err == nil && len(regs) > 0 {
- ctx.Redirect(setting.AppSubURL + "/user/u2f")
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/two_factor")
-}
-
-// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
-func LinkAccountPostRegister(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RegisterForm)
- // TODO Make insecure passwords optional for local accounts also,
- // once email-based Second-Factor Auth is available
- ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
- ctx.Data["Title"] = ctx.Tr("link_account")
- ctx.Data["LinkAccountMode"] = true
- ctx.Data["LinkAccountModeRegister"] = true
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
- 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["DisableRegistration"] = setting.Service.DisableRegistration
- ctx.Data["ShowRegistrationButton"] = false
-
- // use this to set the right link into the signIn and signUp templates in the link_account template
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
- ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
-
- gothUserInterface := ctx.Session.Get("linkAccountGothUser")
- if gothUserInterface == nil {
- ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
- return
- }
- gothUser, ok := gothUserInterface.(goth.User)
- if !ok {
- ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplLinkAccount)
- return
- }
-
- if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
- var valid bool
- var err error
- switch setting.Service.CaptchaType {
- case setting.ImageCaptcha:
- valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
- case setting.ReCaptcha:
- valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
- case setting.HCaptcha:
- 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"), tplLinkAccount, &form)
- return
- }
- }
-
- if !form.IsEmailDomainAllowed() {
- ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
- return
- }
-
- if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
- // In models.User an empty password is classed as not set, so we set form.Password to empty.
- // Eventually the database should be changed to indicate "Second Factor"-enabled accounts
- // (accounts that do not introduce the security vulnerabilities of a password).
- // If a user decides to circumvent second-factor security, and purposefully create a password,
- // they can still do so using the "Recover Account" option.
- form.Password = ""
- } else {
- if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
- return
- }
- if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
- return
- }
- }
-
- loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider)
- if err != nil {
- ctx.ServerError("CreateUser", err)
- }
-
- u := &models.User{
- Name: form.UserName,
- Email: form.Email,
- Passwd: form.Password,
- IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
- LoginType: models.LoginOAuth2,
- LoginSource: loginSource.ID,
- LoginName: gothUser.UserID,
- }
-
- if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, &gothUser, false) {
- // error already handled
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/login")
-}
-
-// HandleSignOut resets the session and sets the cookies
-func HandleSignOut(ctx *context.Context) {
- _ = ctx.Session.Flush()
- _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
- ctx.DeleteCookie(setting.CookieUserName)
- ctx.DeleteCookie(setting.CookieRememberName)
- middleware.DeleteCSRFCookie(ctx.Resp)
- middleware.DeleteLocaleCookie(ctx.Resp)
- middleware.DeleteRedirectToCookie(ctx.Resp)
-}
-
-// SignOut sign out from login status
-func SignOut(ctx *context.Context) {
- if ctx.User != nil {
- eventsource.GetManager().SendMessageBlocking(ctx.User.ID, &eventsource.Event{
- Name: "logout",
- Data: ctx.Session.ID(),
- })
- }
- HandleSignOut(ctx)
- ctx.Redirect(setting.AppSubURL + "/")
-}
-
-// SignUp render the register page
-func SignUp(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("sign_up")
-
- ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
-
- 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["PageIsSignUp"] = true
-
- //Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
- ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
-
- ctx.HTML(http.StatusOK, tplSignUp)
-}
-
-// SignUpPost response for sign up information submission
-func SignUpPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RegisterForm)
- ctx.Data["Title"] = ctx.Tr("sign_up")
-
- ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
-
- 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["PageIsSignUp"] = true
-
- //Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
- if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
- ctx.Error(http.StatusForbidden)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplSignUp)
- 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:
- valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
- case setting.HCaptcha:
- 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"), tplSignUp, &form)
- return
- }
- }
-
- if !form.IsEmailDomainAllowed() {
- ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
- return
- }
-
- if form.Password != form.Retype {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
- return
- }
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
- return
- }
- if !password.IsComplexEnough(form.Password) {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
- return
- }
- pwned, err := password.IsPwned(ctx, form.Password)
- if pwned {
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(errMsg, tplSignUp, &form)
- return
- }
-
- u := &models.User{
- Name: form.UserName,
- Email: form.Email,
- Passwd: form.Password,
- IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
- }
-
- if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, false) {
- // error already handled
- return
- }
-
- ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
- handleSignInFull(ctx, u, false, true)
-}
-
-// createAndHandleCreatedUser calls createUserInContext and
-// then handleUserCreated.
-func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) bool {
- if !createUserInContext(ctx, tpl, form, u, gothUser, allowLink) {
- return false
- }
- return handleUserCreated(ctx, u, gothUser)
-}
-
-// createUserInContext creates a user and handles errors within a given context.
-// Optionally a template can be specified.
-func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) (ok bool) {
- if err := models.CreateUser(u); err != nil {
- if allowLink && (models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err)) {
- if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
- var user *models.User
- user = &models.User{Name: u.Name}
- hasUser, err := models.GetUser(user)
- if !hasUser || err != nil {
- user = &models.User{Email: u.Email}
- hasUser, err = models.GetUser(user)
- if !hasUser || err != nil {
- ctx.ServerError("UserLinkAccount", err)
- return
- }
- }
-
- // TODO: probably we should respect 'remember' user's choice...
- linkAccount(ctx, user, *gothUser, true)
- return // user is already created here, all redirects are handled
- } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
- showLinkingLogin(ctx, *gothUser)
- return // user will be created only after linking login
- }
- }
-
- // handle error without template
- if len(tpl) == 0 {
- ctx.ServerError("CreateUser", err)
- return
- }
-
- // handle error with template
- switch {
- case models.IsErrUserAlreadyExist(err):
- ctx.Data["Err_UserName"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
- case models.IsErrEmailAlreadyUsed(err):
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
- case models.IsErrEmailInvalid(err):
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
- case models.IsErrNameReserved(err):
- ctx.Data["Err_UserName"] = true
- ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
- case models.IsErrNamePatternNotAllowed(err):
- ctx.Data["Err_UserName"] = true
- ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
- case models.IsErrNameCharsNotAllowed(err):
- ctx.Data["Err_UserName"] = true
- ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tpl, form)
- default:
- ctx.ServerError("CreateUser", err)
- }
- return
- }
- log.Trace("Account created: %s", u.Name)
- return true
-}
-
-// handleUserCreated does additional steps after a new user is created.
-// It auto-sets admin for the only user, updates the optional external user and
-// sends a confirmation email if required.
-func handleUserCreated(ctx *context.Context, u *models.User, gothUser *goth.User) (ok bool) {
- // Auto-set admin for the only user.
- if models.CountUsers() == 1 {
- u.IsAdmin = true
- u.IsActive = true
- u.SetLastLogin()
- if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- }
-
- // update external user information
- if gothUser != nil {
- if err := models.UpdateExternalUser(u, *gothUser); err != nil {
- log.Error("UpdateExternalUser failed: %v", err)
- }
- }
-
- // Send confirmation email
- if !u.IsActive && u.ID > 1 {
- mailer.SendActivateAccountMail(ctx.Locale, u)
-
- ctx.Data["IsSendRegisterMail"] = true
- ctx.Data["Email"] = u.Email
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
- ctx.HTML(http.StatusOK, TplActivate)
-
- if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
- return
- }
-
- return true
-}
-
-// Activate render activate user page
-func Activate(ctx *context.Context) {
- code := ctx.Query("code")
-
- if len(code) == 0 {
- ctx.Data["IsActivatePage"] = true
- if ctx.User == nil || ctx.User.IsActive {
- ctx.NotFound("invalid user", nil)
- return
- }
- // Resend confirmation email.
- if setting.Service.RegisterEmailConfirm {
- if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
- ctx.Data["ResendLimited"] = true
- } else {
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
- mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
-
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
- }
- } else {
- ctx.Data["ServiceNotEnabled"] = true
- }
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
-
- user := models.VerifyUserActiveCode(code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsActivateFailed"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
-
- // if account is local account, verify password
- if user.LoginSource == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
-
- handleAccountActivation(ctx, user)
-}
-
-// ActivatePost handles account activation with password check
-func ActivatePost(ctx *context.Context) {
- code := ctx.Query("code")
- if len(code) == 0 {
- ctx.Redirect(setting.AppSubURL + "/user/activate")
- return
- }
-
- user := models.VerifyUserActiveCode(code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsActivateFailed"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
-
- // if account is local account, verify password
- if user.LoginSource == 0 {
- password := ctx.Query("password")
- if len(password) == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
- if !user.ValidatePassword(password) {
- ctx.Data["IsActivateFailed"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return
- }
- }
-
- handleAccountActivation(ctx, user)
-}
-
-func handleAccountActivation(ctx *context.Context, user *models.User) {
- user.IsActive = true
- var err error
- if user.Rands, err = models.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.NotFound("UpdateUserCols", err)
- } else {
- ctx.ServerError("UpdateUser", err)
- }
- return
- }
-
- log.Trace("User activated: %s", user.Name)
-
- if err := ctx.Session.Set("uid", user.ID); err != nil {
- log.Error(fmt.Sprintf("Error setting uid in session: %v", err))
- }
- if err := ctx.Session.Set("uname", user.Name); err != nil {
- log.Error(fmt.Sprintf("Error setting uname in session: %v", err))
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
- ctx.Flash.Success(ctx.Tr("auth.account_activated"))
- ctx.Redirect(setting.AppSubURL + "/")
-}
-
-// ActivateEmail render the activate email page
-func ActivateEmail(ctx *context.Context) {
- code := ctx.Query("code")
- emailStr := ctx.Query("email")
-
- // Verify code.
- if email := models.VerifyActiveEmailCode(code, emailStr); email != nil {
- if err := email.Activate(); err != nil {
- ctx.ServerError("ActivateEmail", err)
- }
-
- log.Trace("Email activated: %s", email.Email)
- ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
-
- if u, err := models.GetUserByID(email.UID); err != nil {
- log.Warn("GetUserByID: %d", email.UID)
- } else {
- // Allow user to validate more emails
- _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
- }
- }
-
- // FIXME: e-mail verification does not require the user to be logged in,
- // so this could be redirecting to the login page.
- // Should users be logged in automatically here? (consider 2FA requirements, etc.)
- ctx.Redirect(setting.AppSubURL + "/user/settings/account")
-}
-
-// ForgotPasswd render the forget pasword page
-func ForgotPasswd(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
-
- if setting.MailService == nil {
- ctx.Data["IsResetDisable"] = true
- ctx.HTML(http.StatusOK, tplForgotPassword)
- return
- }
-
- email := ctx.Query("email")
- ctx.Data["Email"] = email
-
- ctx.Data["IsResetRequest"] = true
- ctx.HTML(http.StatusOK, tplForgotPassword)
-}
-
-// ForgotPasswdPost response for forget password request
-func ForgotPasswdPost(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
-
- if setting.MailService == nil {
- ctx.NotFound("ForgotPasswdPost", nil)
- return
- }
- ctx.Data["IsResetRequest"] = true
-
- email := ctx.Query("email")
- ctx.Data["Email"] = email
-
- u, err := models.GetUserByEmail(email)
- if err != nil {
- if models.IsErrUserNotExist(err) {
- ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
- ctx.Data["IsResetSent"] = true
- ctx.HTML(http.StatusOK, tplForgotPassword)
- return
- }
-
- ctx.ServerError("user.ResetPasswd(check existence)", err)
- return
- }
-
- if !u.IsLocal() && !u.IsOAuth2() {
- ctx.Data["Err_Email"] = true
- ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
- return
- }
-
- if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
- ctx.Data["ResendLimited"] = true
- ctx.HTML(http.StatusOK, tplForgotPassword)
- return
- }
-
- mailer.SendResetPasswordMail(u)
-
- if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
-
- ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
- ctx.Data["IsResetSent"] = true
- ctx.HTML(http.StatusOK, tplForgotPassword)
-}
-
-func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
- code := ctx.Query("code")
-
- ctx.Data["Title"] = ctx.Tr("auth.reset_password")
- ctx.Data["Code"] = code
-
- if nil != ctx.User {
- ctx.Data["user_signed_in"] = true
- }
-
- if len(code) == 0 {
- ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
- return nil, nil
- }
-
- // Fail early, don't frustrate the user
- u := models.VerifyUserActiveCode(code)
- if u == nil {
- ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
- return nil, nil
- }
-
- twofa, err := models.GetTwoFactorByUID(u.ID)
- if err != nil {
- if !models.IsErrTwoFactorNotEnrolled(err) {
- ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
- return nil, nil
- }
- } else {
- ctx.Data["has_two_factor"] = true
- ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
- }
-
- // Show the user that they are affecting the account that they intended to
- ctx.Data["user_email"] = u.Email
-
- if nil != ctx.User && u.ID != ctx.User.ID {
- ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
- return nil, nil
- }
-
- return u, twofa
-}
-
-// ResetPasswd render the account recovery page
-func ResetPasswd(ctx *context.Context) {
- ctx.Data["IsResetForm"] = true
-
- commonResetPassword(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplResetPassword)
-}
-
-// ResetPasswdPost response from account recovery request
-func ResetPasswdPost(ctx *context.Context) {
- u, twofa := commonResetPassword(ctx)
- if ctx.Written() {
- return
- }
-
- if u == nil {
- // Flash error has been set
- ctx.HTML(http.StatusOK, tplResetPassword)
- return
- }
-
- // Validate password length.
- passwd := ctx.Query("password")
- if len(passwd) < setting.MinPasswordLength {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
- return
- } else if !password.IsComplexEnough(passwd) {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
- return
- } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
- errMsg := ctx.Tr("auth.password_pwned")
- if err != nil {
- log.Error(err.Error())
- errMsg = ctx.Tr("auth.password_pwned_err")
- }
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(errMsg, tplResetPassword, nil)
- return
- }
-
- // Handle two-factor
- regenerateScratchToken := false
- if twofa != nil {
- if ctx.QueryBool("scratch_code") {
- if !twofa.VerifyScratchToken(ctx.Query("token")) {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Token"] = true
- ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
- return
- }
- regenerateScratchToken = true
- } else {
- passcode := ctx.Query("passcode")
- ok, err := twofa.ValidateTOTP(passcode)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
- return
- }
- if !ok || twofa.LastUsedPasscode == passcode {
- ctx.Data["IsResetForm"] = true
- ctx.Data["Err_Passcode"] = true
- ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
- return
- }
-
- twofa.LastUsedPasscode = passcode
- if err = models.UpdateTwoFactor(twofa); err != nil {
- ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
- return
- }
- }
- }
- var err error
- if u.Rands, err = models.GetUserSalt(); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- if err = u.SetPassword(passwd); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- u.MustChangePassword = false
- if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- log.Trace("User password reset: %s", u.Name)
- ctx.Data["IsResetFailed"] = true
- remember := len(ctx.Query("remember")) != 0
-
- if regenerateScratchToken {
- // Invalidate the scratch token.
- _, err = twofa.GenerateScratchToken()
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if err = models.UpdateTwoFactor(twofa); err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- handleSignInFull(ctx, u, remember, false)
- ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
- return
- }
-
- handleSignInFull(ctx, u, remember, true)
-}
-
-// MustChangePassword renders the page to change a user's password
-func MustChangePassword(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
- ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
- ctx.Data["MustChangePassword"] = true
- ctx.HTML(http.StatusOK, tplMustChangePassword)
-}
-
-// MustChangePasswordPost response for updating a user's password after his/her
-// account was created by an admin
-func MustChangePasswordPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
- ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
- ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplMustChangePassword)
- return
- }
- u := ctx.User
- // Make sure only requests for users who are eligible to change their password via
- // this method passes through
- if !u.MustChangePassword {
- ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
- return
- }
-
- if form.Password != form.Retype {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
- return
- }
-
- if len(form.Password) < setting.MinPasswordLength {
- ctx.Data["Err_Password"] = true
- ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
- return
- }
-
- var err error
- if err = u.SetPassword(form.Password); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- u.MustChangePassword = false
-
- if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
-
- log.Trace("User updated password: %s", u.Name)
-
- if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo)
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/")
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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 = ©OfGravatarSourceURL
- 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))
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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("..", ".."))
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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))
- }
-}
+++ /dev/null
-// 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/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())
-}
+++ /dev/null
-// 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())
- }
-}
+++ /dev/null
-// 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())
- }
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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
- }
- }
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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("..", "..", ".."))
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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)
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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")
-}
+++ /dev/null
-// 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",
- })
-}
+++ /dev/null
-// 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,
- })
-}
--- /dev/null
+// 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))
+}
--- /dev/null
+// 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))
+ }
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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())
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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("..", "..", ".."))
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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))
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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)
+ })
+ }
+}
--- /dev/null
+// 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("*")))
+}
--- /dev/null
+// 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()
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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,
+ })))
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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 + "/")
+ }
+}
--- /dev/null
+// 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())
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+ }
+}
--- /dev/null
+// 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">​</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())
+}
--- /dev/null
+// 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))
+}
--- /dev/null
+// 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
+ }
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+ }
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+ }
+}
--- /dev/null
+// 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")
+ }
+}
--- /dev/null
+// 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 (
+ "bytes"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "path"
+ "strconv"
+ "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/git"
+ 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"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/upload"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ comment_service "code.gitea.io/gitea/services/comments"
+ "code.gitea.io/gitea/services/forms"
+ issue_service "code.gitea.io/gitea/services/issue"
+ pull_service "code.gitea.io/gitea/services/pull"
+
+ "github.com/unknwon/com"
+)
+
+const (
+ tplAttachment base.TplName = "repo/issue/view_content/attachments"
+
+ tplIssues base.TplName = "repo/issue/list"
+ tplIssueNew base.TplName = "repo/issue/new"
+ tplIssueChoose base.TplName = "repo/issue/choose"
+ tplIssueView base.TplName = "repo/issue/view"
+
+ tplReactions base.TplName = "repo/issue/view_content/reactions"
+
+ issueTemplateKey = "IssueTemplate"
+ issueTemplateTitleKey = "IssueTemplateTitle"
+)
+
+var (
+ // IssueTemplateCandidates issue templates
+ IssueTemplateCandidates = []string{
+ "ISSUE_TEMPLATE.md",
+ "issue_template.md",
+ ".gitea/ISSUE_TEMPLATE.md",
+ ".gitea/issue_template.md",
+ ".github/ISSUE_TEMPLATE.md",
+ ".github/issue_template.md",
+ }
+)
+
+// MustAllowUserComment checks to make sure if an issue is locked.
+// If locked and user has permissions to write to the repository,
+// then the comment is allowed, else it is blocked
+func MustAllowUserComment(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
+ ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
+ ctx.Redirect(issue.HTMLURL())
+ return
+ }
+}
+
+// MustEnableIssues check if repository enable internal issues
+func MustEnableIssues(ctx *context.Context) {
+ if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
+ !ctx.Repo.CanRead(models.UnitTypeExternalTracker) {
+ ctx.NotFound("MustEnableIssues", nil)
+ return
+ }
+
+ unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
+ if err == nil {
+ ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
+ return
+ }
+}
+
+// MustAllowPulls check if repository enable pull requests and user have right to do that
+func MustAllowPulls(ctx *context.Context) {
+ if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(models.UnitTypePullRequests) {
+ ctx.NotFound("MustAllowPulls", nil)
+ return
+ }
+
+ // User can send pull request if owns a forked repository.
+ if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) {
+ ctx.Repo.PullRequest.Allowed = true
+ ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName
+ }
+}
+
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+ var err error
+ viewType := ctx.Query("type")
+ sortType := ctx.Query("sort")
+ types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
+ if !util.IsStringInSlice(viewType, types, true) {
+ viewType = "all"
+ }
+
+ var (
+ assigneeID = ctx.QueryInt64("assignee")
+ posterID int64
+ mentionedID int64
+ reviewRequestedID int64
+ forceEmpty bool
+ )
+
+ if ctx.IsSigned {
+ switch viewType {
+ case "created_by":
+ posterID = ctx.User.ID
+ case "mentioned":
+ mentionedID = ctx.User.ID
+ case "assigned":
+ assigneeID = ctx.User.ID
+ case "review_requested":
+ reviewRequestedID = ctx.User.ID
+ }
+ }
+
+ repo := ctx.Repo.Repository
+ var labelIDs []int64
+ selectLabels := ctx.Query("labels")
+ if len(selectLabels) > 0 && selectLabels != "0" {
+ labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
+ if err != nil {
+ ctx.ServerError("StringsToInt64s", err)
+ return
+ }
+ }
+
+ keyword := strings.Trim(ctx.Query("q"), " ")
+ if bytes.Contains([]byte(keyword), []byte{0x00}) {
+ keyword = ""
+ }
+
+ var issueIDs []int64
+ if len(keyword) > 0 {
+ issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
+ if err != nil {
+ ctx.ServerError("issueIndexer.Search", err)
+ return
+ }
+ if len(issueIDs) == 0 {
+ forceEmpty = true
+ }
+ }
+
+ var issueStats *models.IssueStats
+ if forceEmpty {
+ issueStats = &models.IssueStats{}
+ } else {
+ issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
+ RepoID: repo.ID,
+ Labels: selectLabels,
+ MilestoneID: milestoneID,
+ AssigneeID: assigneeID,
+ MentionedID: mentionedID,
+ PosterID: posterID,
+ ReviewRequestedID: reviewRequestedID,
+ IsPull: isPullOption,
+ IssueIDs: issueIDs,
+ })
+ if err != nil {
+ ctx.ServerError("GetIssueStats", err)
+ return
+ }
+ }
+
+ isShowClosed := ctx.Query("state") == "closed"
+ // if open issues are zero and close don't, use closed as default
+ if len(ctx.Query("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
+ isShowClosed = true
+ }
+
+ page := ctx.QueryInt("page")
+ if page <= 1 {
+ page = 1
+ }
+
+ var total int
+ if !isShowClosed {
+ total = int(issueStats.OpenCount)
+ } else {
+ total = int(issueStats.ClosedCount)
+ }
+ pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+
+ var mileIDs []int64
+ if milestoneID > 0 {
+ mileIDs = []int64{milestoneID}
+ }
+
+ var issues []*models.Issue
+ if forceEmpty {
+ issues = []*models.Issue{}
+ } else {
+ issues, err = models.Issues(&models.IssuesOptions{
+ ListOptions: models.ListOptions{
+ Page: pager.Paginater.Current(),
+ PageSize: setting.UI.IssuePagingNum,
+ },
+ RepoIDs: []int64{repo.ID},
+ AssigneeID: assigneeID,
+ PosterID: posterID,
+ MentionedID: mentionedID,
+ ReviewRequestedID: reviewRequestedID,
+ MilestoneIDs: mileIDs,
+ ProjectID: projectID,
+ IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsPull: isPullOption,
+ LabelIDs: labelIDs,
+ SortType: sortType,
+ IssueIDs: issueIDs,
+ })
+ if err != nil {
+ ctx.ServerError("Issues", err)
+ return
+ }
+ }
+
+ var issueList = models.IssueList(issues)
+ approvalCounts, err := issueList.GetApprovalCounts()
+ if err != nil {
+ ctx.ServerError("ApprovalCounts", err)
+ return
+ }
+
+ // Get posters.
+ for i := range issues {
+ // Check read status
+ if !ctx.IsSigned {
+ issues[i].IsRead = true
+ } else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
+ ctx.ServerError("GetIsRead", err)
+ return
+ }
+ }
+
+ commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
+ if err != nil {
+ ctx.ServerError("GetIssuesLastCommitStatus", err)
+ return
+ }
+
+ ctx.Data["Issues"] = issues
+ ctx.Data["CommitStatus"] = commitStatus
+
+ // Get assignees.
+ ctx.Data["Assignees"], err = repo.GetAssignees()
+ if err != nil {
+ ctx.ServerError("GetAssignees", err)
+ return
+ }
+
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByRepoID", err)
+ return
+ }
+
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+
+ ctx.Data["OrgLabels"] = orgLabels
+ labels = append(labels, orgLabels...)
+ }
+
+ for _, l := range labels {
+ l.LoadSelectedLabelsAfterClick(labelIDs)
+ }
+ ctx.Data["Labels"] = labels
+ ctx.Data["NumLabels"] = len(labels)
+
+ if ctx.QueryInt64("assignee") == 0 {
+ assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
+ }
+
+ ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
+ issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
+
+ 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["IssueStats"] = issueStats
+ ctx.Data["SelLabelIDs"] = labelIDs
+ ctx.Data["SelectLabels"] = selectLabels
+ ctx.Data["ViewType"] = viewType
+ ctx.Data["SortType"] = sortType
+ ctx.Data["MilestoneID"] = milestoneID
+ ctx.Data["AssigneeID"] = assigneeID
+ ctx.Data["IsShowClosed"] = isShowClosed
+ ctx.Data["Keyword"] = keyword
+ if isShowClosed {
+ ctx.Data["State"] = "closed"
+ } else {
+ ctx.Data["State"] = "open"
+ }
+
+ pager.AddParam(ctx, "q", "Keyword")
+ pager.AddParam(ctx, "type", "ViewType")
+ 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
+}
+
+// Issues render issues page
+func Issues(ctx *context.Context) {
+ isPullList := ctx.Params(":type") == "pulls"
+ if isPullList {
+ MustAllowPulls(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Title"] = ctx.Tr("repo.pulls")
+ ctx.Data["PageIsPullList"] = true
+ } else {
+ MustEnableIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Title"] = ctx.Tr("repo.issues")
+ ctx.Data["PageIsIssueList"] = true
+ ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+ }
+
+ issues(ctx, ctx.QueryInt64("milestone"), ctx.QueryInt64("project"), util.OptionalBoolOf(isPullList))
+ if ctx.Written() {
+ return
+ }
+
+ var err error
+ // Get milestones
+ ctx.Data["Milestones"], err = models.GetMilestones(models.GetMilestonesOption{
+ RepoID: ctx.Repo.Repository.ID,
+ State: api.StateType(ctx.Query("state")),
+ })
+ if err != nil {
+ ctx.ServerError("GetAllRepoMilestones", err)
+ return
+ }
+
+ ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
+
+ ctx.HTML(http.StatusOK, tplIssues)
+}
+
+// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
+func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
+ var err error
+ ctx.Data["OpenMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+ RepoID: repo.ID,
+ State: api.StateOpen,
+ })
+ if err != nil {
+ ctx.ServerError("GetMilestones", err)
+ return
+ }
+ ctx.Data["ClosedMilestones"], err = models.GetMilestones(models.GetMilestonesOption{
+ RepoID: repo.ID,
+ State: api.StateClosed,
+ })
+ if err != nil {
+ ctx.ServerError("GetMilestones", err)
+ return
+ }
+
+ ctx.Data["Assignees"], err = repo.GetAssignees()
+ if err != nil {
+ ctx.ServerError("GetAssignees", err)
+ return
+ }
+
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
+}
+
+func retrieveProjects(ctx *context.Context, repo *models.Repository) {
+
+ var err error
+
+ ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
+ RepoID: repo.ID,
+ Page: -1,
+ IsClosed: util.OptionalBoolFalse,
+ Type: models.ProjectTypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+
+ ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
+ RepoID: repo.ID,
+ Page: -1,
+ IsClosed: util.OptionalBoolTrue,
+ Type: models.ProjectTypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+}
+
+// repoReviewerSelection items to bee shown
+type repoReviewerSelection struct {
+ IsTeam bool
+ Team *models.Team
+ User *models.User
+ Review *models.Review
+ CanChange bool
+ Checked bool
+ ItemID int64
+}
+
+// RetrieveRepoReviewers find all reviewers of a repository
+func RetrieveRepoReviewers(ctx *context.Context, repo *models.Repository, issue *models.Issue, canChooseReviewer bool) {
+ ctx.Data["CanChooseReviewer"] = canChooseReviewer
+
+ originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
+ if err != nil {
+ ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
+ return
+ }
+ ctx.Data["OriginalReviews"] = originalAuthorReviews
+
+ reviews, err := models.GetReviewersByIssueID(issue.ID)
+ if err != nil {
+ ctx.ServerError("GetReviewersByIssueID", err)
+ return
+ }
+
+ if len(reviews) == 0 && !canChooseReviewer {
+ return
+ }
+
+ var (
+ pullReviews []*repoReviewerSelection
+ reviewersResult []*repoReviewerSelection
+ teamReviewersResult []*repoReviewerSelection
+ teamReviewers []*models.Team
+ reviewers []*models.User
+ )
+
+ if canChooseReviewer {
+ posterID := issue.PosterID
+ if issue.OriginalAuthorID > 0 {
+ posterID = 0
+ }
+
+ reviewers, err = repo.GetReviewers(ctx.User.ID, posterID)
+ if err != nil {
+ ctx.ServerError("GetReviewers", err)
+ return
+ }
+
+ teamReviewers, err = repo.GetReviewerTeams()
+ if err != nil {
+ ctx.ServerError("GetReviewerTeams", err)
+ return
+ }
+
+ if len(reviewers) > 0 {
+ reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
+ }
+
+ if len(teamReviewers) > 0 {
+ teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
+ }
+ }
+
+ pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
+
+ for _, review := range reviews {
+ tmp := &repoReviewerSelection{
+ Checked: review.Type == models.ReviewTypeRequest,
+ Review: review,
+ ItemID: review.ReviewerID,
+ }
+ if review.ReviewerTeamID > 0 {
+ tmp.IsTeam = true
+ tmp.ItemID = -review.ReviewerTeamID
+ }
+
+ if ctx.Repo.IsAdmin() {
+ // Admin can dismiss or re-request any review requests
+ tmp.CanChange = true
+ } else if ctx.User != nil && ctx.User.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
+ // A user can refuse review requests
+ tmp.CanChange = true
+ } else if (canChooseReviewer || (ctx.User != nil && ctx.User.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
+ ctx.User.ID != review.ReviewerID {
+ // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
+ tmp.CanChange = true
+ }
+
+ pullReviews = append(pullReviews, tmp)
+
+ if canChooseReviewer {
+ if tmp.IsTeam {
+ teamReviewersResult = append(teamReviewersResult, tmp)
+ } else {
+ reviewersResult = append(reviewersResult, tmp)
+ }
+ }
+ }
+
+ if len(pullReviews) > 0 {
+ // Drop all non-existing users and teams from the reviews
+ currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
+ for _, item := range pullReviews {
+ if item.Review.ReviewerID > 0 {
+ if err = item.Review.LoadReviewer(); err != nil {
+ if models.IsErrUserNotExist(err) {
+ continue
+ }
+ ctx.ServerError("LoadReviewer", err)
+ return
+ }
+ item.User = item.Review.Reviewer
+ } else if item.Review.ReviewerTeamID > 0 {
+ if err = item.Review.LoadReviewerTeam(); err != nil {
+ if models.IsErrTeamNotExist(err) {
+ continue
+ }
+ ctx.ServerError("LoadReviewerTeam", err)
+ return
+ }
+ item.Team = item.Review.ReviewerTeam
+ } else {
+ continue
+ }
+
+ currentPullReviewers = append(currentPullReviewers, item)
+ }
+ ctx.Data["PullReviewers"] = currentPullReviewers
+ }
+
+ if canChooseReviewer && reviewersResult != nil {
+ preadded := len(reviewersResult)
+ for _, reviewer := range reviewers {
+ found := false
+ reviewAddLoop:
+ for _, tmp := range reviewersResult[:preadded] {
+ if tmp.ItemID == reviewer.ID {
+ tmp.User = reviewer
+ found = true
+ break reviewAddLoop
+ }
+ }
+
+ if found {
+ continue
+ }
+
+ reviewersResult = append(reviewersResult, &repoReviewerSelection{
+ IsTeam: false,
+ CanChange: true,
+ User: reviewer,
+ ItemID: reviewer.ID,
+ })
+ }
+
+ ctx.Data["Reviewers"] = reviewersResult
+ }
+
+ if canChooseReviewer && teamReviewersResult != nil {
+ preadded := len(teamReviewersResult)
+ for _, team := range teamReviewers {
+ found := false
+ teamReviewAddLoop:
+ for _, tmp := range teamReviewersResult[:preadded] {
+ if tmp.ItemID == -team.ID {
+ tmp.Team = team
+ found = true
+ break teamReviewAddLoop
+ }
+ }
+
+ if found {
+ continue
+ }
+
+ teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
+ IsTeam: true,
+ CanChange: true,
+ Team: team,
+ ItemID: -team.ID,
+ })
+ }
+
+ ctx.Data["TeamReviewers"] = teamReviewersResult
+ }
+}
+
+// RetrieveRepoMetas find all the meta information of a repository
+func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull bool) []*models.Label {
+ if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
+ return nil
+ }
+
+ labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByRepoID", err)
+ return nil
+ }
+ ctx.Data["Labels"] = labels
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ return nil
+ }
+
+ ctx.Data["OrgLabels"] = orgLabels
+ labels = append(labels, orgLabels...)
+ }
+
+ RetrieveRepoMilestonesAndAssignees(ctx, repo)
+ if ctx.Written() {
+ return nil
+ }
+
+ retrieveProjects(ctx, repo)
+ if ctx.Written() {
+ return nil
+ }
+
+ brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0)
+ if err != nil {
+ ctx.ServerError("GetBranches", err)
+ return nil
+ }
+ ctx.Data["Branches"] = brs
+
+ // Contains true if the user can create issue dependencies
+ ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
+
+ return labels
+}
+
+func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
+ var bytes []byte
+
+ if ctx.Repo.Commit == nil {
+ var err error
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+ if err != nil {
+ return "", false
+ }
+ }
+
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
+ if err != nil {
+ return "", false
+ }
+ if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
+ return "", false
+ }
+ r, err := entry.Blob().DataAsync()
+ if err != nil {
+ return "", false
+ }
+ defer r.Close()
+ bytes, err = ioutil.ReadAll(r)
+ if err != nil {
+ return "", false
+ }
+ return string(bytes), true
+}
+
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs []string, possibleFiles []string) {
+ templateCandidates := make([]string, 0, len(possibleFiles))
+ if ctx.Query("template") != "" {
+ for _, dirName := range possibleDirs {
+ templateCandidates = append(templateCandidates, path.Join(dirName, ctx.Query("template")))
+ }
+ }
+ templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
+ for _, filename := range templateCandidates {
+ templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
+ if found {
+ var meta api.IssueTemplate
+ templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
+ if err != nil {
+ log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
+ ctx.Data[ctxDataKey] = templateContent
+ return
+ }
+ ctx.Data[issueTemplateTitleKey] = meta.Title
+ ctx.Data[ctxDataKey] = templateBody
+ labelIDs := make([]string, 0, len(meta.Labels))
+ if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", models.ListOptions{}); err == nil {
+ ctx.Data["Labels"] = repoLabels
+ if ctx.Repo.Owner.IsOrganization() {
+ if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{}); err == nil {
+ ctx.Data["OrgLabels"] = orgLabels
+ repoLabels = append(repoLabels, orgLabels...)
+ }
+ }
+
+ for _, metaLabel := range meta.Labels {
+ for _, repoLabel := range repoLabels {
+ if strings.EqualFold(repoLabel.Name, metaLabel) {
+ repoLabel.IsChecked = true
+ labelIDs = append(labelIDs, fmt.Sprintf("%d", repoLabel.ID))
+ break
+ }
+ }
+ }
+ }
+ ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
+ ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+ return
+ }
+ }
+}
+
+// NewIssue render creating issue page
+func NewIssue(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+ ctx.Data["PageIsIssueList"] = true
+ ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+ ctx.Data["RequireHighlightJS"] = true
+ ctx.Data["RequireSimpleMDE"] = true
+ ctx.Data["RequireTribute"] = true
+ ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+ title := ctx.Query("title")
+ ctx.Data["TitleQuery"] = title
+ body := ctx.Query("body")
+ ctx.Data["BodyQuery"] = body
+
+ ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
+ milestoneID := ctx.QueryInt64("milestone")
+ if milestoneID > 0 {
+ milestone, err := models.GetMilestoneByID(milestoneID)
+ if err != nil {
+ log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
+ } else {
+ ctx.Data["milestone_id"] = milestoneID
+ ctx.Data["Milestone"] = milestone
+ }
+ }
+
+ projectID := ctx.QueryInt64("project")
+ if projectID > 0 {
+ project, err := models.GetProjectByID(projectID)
+ if err != nil {
+ log.Error("GetProjectByID: %d: %v", projectID, err)
+ } else if project.RepoID != ctx.Repo.Repository.ID {
+ log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
+ } else {
+ ctx.Data["project_id"] = projectID
+ ctx.Data["Project"] = project
+ }
+
+ }
+
+ RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
+ setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeIssues)
+
+ ctx.HTML(http.StatusOK, tplIssueNew)
+}
+
+// NewIssueChooseTemplate render creating issue from template page
+func NewIssueChooseTemplate(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+ ctx.Data["PageIsIssueList"] = true
+ ctx.Data["milestone"] = ctx.QueryInt64("milestone")
+
+ issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
+ ctx.Data["NewIssueChooseTemplate"] = len(issueTemplates) > 0
+ ctx.Data["IssueTemplates"] = issueTemplates
+
+ ctx.HTML(http.StatusOK, tplIssueChoose)
+}
+
+// ValidateRepoMetas check and returns repository's meta informations
+func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
+ var (
+ repo = ctx.Repo.Repository
+ err error
+ )
+
+ labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
+ if ctx.Written() {
+ return nil, nil, 0, 0
+ }
+
+ var labelIDs []int64
+ hasSelected := false
+ // Check labels.
+ if len(form.LabelIDs) > 0 {
+ labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
+ if err != nil {
+ return nil, nil, 0, 0
+ }
+ labelIDMark := base.Int64sToMap(labelIDs)
+
+ for i := range labels {
+ if labelIDMark[labels[i].ID] {
+ labels[i].IsChecked = true
+ hasSelected = true
+ }
+ }
+ }
+
+ ctx.Data["Labels"] = labels
+ ctx.Data["HasSelectedLabel"] = hasSelected
+ ctx.Data["label_ids"] = form.LabelIDs
+
+ // Check milestone.
+ milestoneID := form.MilestoneID
+ if milestoneID > 0 {
+ ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
+ if err != nil {
+ ctx.ServerError("GetMilestoneByID", err)
+ return nil, nil, 0, 0
+ }
+ ctx.Data["milestone_id"] = milestoneID
+ }
+
+ if form.ProjectID > 0 {
+ p, err := models.GetProjectByID(form.ProjectID)
+ if err != nil {
+ ctx.ServerError("GetProjectByID", err)
+ return nil, nil, 0, 0
+ }
+ if p.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("", nil)
+ return nil, nil, 0, 0
+ }
+
+ ctx.Data["Project"] = p
+ ctx.Data["project_id"] = form.ProjectID
+ }
+
+ // Check assignees
+ var assigneeIDs []int64
+ if len(form.AssigneeIDs) > 0 {
+ assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
+ if err != nil {
+ return nil, nil, 0, 0
+ }
+
+ // Check if the passed assignees actually exists and is assignable
+ for _, aID := range assigneeIDs {
+ assignee, err := models.GetUserByID(aID)
+ if err != nil {
+ ctx.ServerError("GetUserByID", err)
+ return nil, nil, 0, 0
+ }
+
+ valid, err := models.CanBeAssigned(assignee, repo, isPull)
+ if err != nil {
+ ctx.ServerError("CanBeAssigned", err)
+ return nil, nil, 0, 0
+ }
+
+ if !valid {
+ ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+ return nil, nil, 0, 0
+ }
+ }
+ }
+
+ // Keep the old assignee id thingy for compatibility reasons
+ if form.AssigneeID > 0 {
+ assigneeIDs = append(assigneeIDs, form.AssigneeID)
+ }
+
+ return labelIDs, assigneeIDs, milestoneID, form.ProjectID
+}
+
+// NewIssuePost response for creating new issue
+func NewIssuePost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateIssueForm)
+ ctx.Data["Title"] = ctx.Tr("repo.issues.new")
+ ctx.Data["PageIsIssueList"] = true
+ ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+ ctx.Data["RequireHighlightJS"] = true
+ ctx.Data["RequireSimpleMDE"] = true
+ ctx.Data["ReadOnly"] = false
+ ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
+ var (
+ repo = ctx.Repo.Repository
+ attachments []string
+ )
+
+ labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
+ if ctx.Written() {
+ return
+ }
+
+ if setting.Attachment.Enabled {
+ attachments = form.Files
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplIssueNew)
+ return
+ }
+
+ if util.IsEmptyString(form.Title) {
+ ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplIssueNew, form)
+ return
+ }
+
+ issue := &models.Issue{
+ RepoID: repo.ID,
+ Title: form.Title,
+ PosterID: ctx.User.ID,
+ Poster: ctx.User,
+ MilestoneID: milestoneID,
+ Content: form.Content,
+ Ref: form.Ref,
+ }
+
+ if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+ if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+ return
+ }
+ ctx.ServerError("NewIssue", err)
+ return
+ }
+
+ if projectID > 0 {
+ if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
+ ctx.ServerError("ChangeProjectAssign", err)
+ return
+ }
+ }
+
+ log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
+ ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+}
+
+// commentTag returns the CommentTag for a comment in/with the given repo, poster and issue
+func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) {
+ perm, err := models.GetUserRepoPermission(repo, poster)
+ if err != nil {
+ return models.CommentTagNone, err
+ }
+ if perm.IsOwner() {
+ if !poster.IsAdmin {
+ return models.CommentTagOwner, nil
+ }
+
+ ok, err := models.IsUserRealRepoAdmin(repo, poster)
+ if err != nil {
+ return models.CommentTagNone, err
+ }
+
+ if ok {
+ return models.CommentTagOwner, nil
+ }
+
+ if ok, err = repo.IsCollaborator(poster.ID); ok && err == nil {
+ return models.CommentTagWriter, nil
+ }
+
+ return models.CommentTagNone, err
+ }
+
+ if perm.CanWrite(models.UnitTypeCode) {
+ return models.CommentTagWriter, nil
+ }
+
+ return models.CommentTagNone, nil
+}
+
+func getBranchData(ctx *context.Context, issue *models.Issue) {
+ ctx.Data["BaseBranch"] = nil
+ ctx.Data["HeadBranch"] = nil
+ ctx.Data["HeadUserName"] = nil
+ ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
+ if issue.IsPull {
+ pull := issue.PullRequest
+ ctx.Data["BaseBranch"] = pull.BaseBranch
+ ctx.Data["HeadBranch"] = pull.HeadBranch
+ ctx.Data["HeadUserName"] = pull.MustHeadUserName()
+ }
+}
+
+// ViewIssue render issue view page
+func ViewIssue(ctx *context.Context) {
+ if ctx.Params(":type") == "issues" {
+ // If issue was requested we check if repo has external tracker and redirect
+ extIssueUnit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
+ if err == nil && extIssueUnit != nil {
+ if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
+ metas := ctx.Repo.Repository.ComposeMetas()
+ metas["index"] = ctx.Params(":index")
+ ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas))
+ return
+ }
+ } else if err != nil && !models.IsErrUnitTypeNotExist(err) {
+ ctx.ServerError("GetUnit", err)
+ return
+ }
+ }
+
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.NotFound("GetIssueByIndex", err)
+ } else {
+ ctx.ServerError("GetIssueByIndex", err)
+ }
+ return
+ }
+
+ // Make sure type and URL matches.
+ if ctx.Params(":type") == "issues" && issue.IsPull {
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
+ ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+ return
+ }
+
+ if issue.IsPull {
+ MustAllowPulls(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["PageIsPullList"] = true
+ ctx.Data["PageIsPullConversation"] = true
+ } else {
+ MustEnableIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["PageIsIssueList"] = true
+ ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
+ }
+
+ if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
+ ctx.Data["IssueType"] = "pulls"
+ } else if !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) {
+ ctx.Data["IssueType"] = "issues"
+ } else {
+ ctx.Data["IssueType"] = "all"
+ }
+
+ ctx.Data["RequireHighlightJS"] = true
+ ctx.Data["RequireTribute"] = true
+ ctx.Data["RequireSimpleMDE"] = true
+ ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(models.UnitTypeProjects)
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
+ if err = issue.LoadAttributes(); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return
+ }
+
+ if err = filterXRefComments(ctx, issue); err != nil {
+ ctx.ServerError("filterXRefComments", err)
+ return
+ }
+
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+
+ iw := new(models.IssueWatch)
+ if ctx.User != nil {
+ iw.UserID = ctx.User.ID
+ iw.IssueID = issue.ID
+ iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
+ if err != nil {
+ ctx.ServerError("CheckIssueWatch", err)
+ return
+ }
+ }
+ ctx.Data["IssueWatch"] = iw
+
+ issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+ URLPrefix: ctx.Repo.RepoLink,
+ Metas: ctx.Repo.Repository.ComposeMetas(),
+ }, issue.Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+
+ repo := ctx.Repo.Repository
+
+ // Get more information if it's a pull request.
+ if issue.IsPull {
+ if issue.PullRequest.HasMerged {
+ ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
+ PrepareMergedViewPullInfo(ctx, issue)
+ } else {
+ PrepareViewPullInfo(ctx, issue)
+ ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
+ }
+ if ctx.Written() {
+ return
+ }
+ }
+
+ // Metas.
+ // Check labels.
+ labelIDMark := make(map[int64]bool)
+ for i := range issue.Labels {
+ labelIDMark[issue.Labels[i].ID] = true
+ }
+ labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByRepoID", err)
+ return
+ }
+ ctx.Data["Labels"] = labels
+
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+ ctx.Data["OrgLabels"] = orgLabels
+
+ labels = append(labels, orgLabels...)
+ }
+
+ hasSelected := false
+ for i := range labels {
+ if labelIDMark[labels[i].ID] {
+ labels[i].IsChecked = true
+ hasSelected = true
+ }
+ }
+ ctx.Data["HasSelectedLabel"] = hasSelected
+
+ // Check milestone and assignee.
+ if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
+ RetrieveRepoMilestonesAndAssignees(ctx, repo)
+ retrieveProjects(ctx, repo)
+
+ if ctx.Written() {
+ return
+ }
+ }
+
+ if issue.IsPull {
+ canChooseReviewer := ctx.Repo.CanWrite(models.UnitTypePullRequests)
+ if !canChooseReviewer && ctx.User != nil && ctx.IsSigned {
+ canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.User)
+ if err != nil {
+ ctx.ServerError("IsOfficialReviewer", err)
+ return
+ }
+ }
+
+ RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
+ if ctx.Written() {
+ return
+ }
+ }
+
+ if ctx.IsSigned {
+ // Update issue-user.
+ if err = issue.ReadBy(ctx.User.ID); err != nil {
+ ctx.ServerError("ReadBy", err)
+ return
+ }
+ }
+
+ var (
+ tag models.CommentTag
+ ok bool
+ marked = make(map[int64]models.CommentTag)
+ comment *models.Comment
+ participants = make([]*models.User, 1, 10)
+ )
+ if ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if ctx.IsSigned {
+ // Deal with the stopwatch
+ ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
+ if !ctx.Data["IsStopwatchRunning"].(bool) {
+ var exists bool
+ var sw *models.Stopwatch
+ if exists, sw, err = models.HasUserStopwatch(ctx.User.ID); err != nil {
+ ctx.ServerError("HasUserStopwatch", err)
+ return
+ }
+ ctx.Data["HasUserStopwatch"] = exists
+ if exists {
+ // Add warning if the user has already a stopwatch
+ var otherIssue *models.Issue
+ if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
+ ctx.ServerError("GetIssueByID", err)
+ return
+ }
+ if err = otherIssue.LoadRepo(); err != nil {
+ ctx.ServerError("LoadRepo", err)
+ return
+ }
+ // Add link to the issue of the already running stopwatch
+ ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
+ }
+ }
+ ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
+ } else {
+ ctx.Data["CanUseTimetracker"] = false
+ }
+ if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+ ctx.ServerError("TotalTimes", err)
+ return
+ }
+ }
+
+ // Check if the user can use the dependencies
+ ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull)
+
+ // check if dependencies can be created across repositories
+ ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
+
+ if issue.ShowTag, err = commentTag(repo, issue.Poster, issue); err != nil {
+ ctx.ServerError("commentTag", err)
+ return
+ }
+ marked[issue.PosterID] = issue.ShowTag
+
+ // Render comments and and fetch participants.
+ participants[0] = issue.Poster
+ for _, comment = range issue.Comments {
+ comment.Issue = issue
+
+ if err := comment.LoadPoster(); err != nil {
+ ctx.ServerError("LoadPoster", err)
+ return
+ }
+
+ if comment.Type == models.CommentTypeComment {
+ if err := comment.LoadAttachments(); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+
+ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+ URLPrefix: ctx.Repo.RepoLink,
+ Metas: ctx.Repo.Repository.ComposeMetas(),
+ }, comment.Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+ // Check tag.
+ tag, ok = marked[comment.PosterID]
+ if ok {
+ comment.ShowTag = tag
+ continue
+ }
+
+ comment.ShowTag, err = commentTag(repo, comment.Poster, issue)
+ if err != nil {
+ ctx.ServerError("commentTag", err)
+ return
+ }
+ marked[comment.PosterID] = comment.ShowTag
+ participants = addParticipant(comment.Poster, participants)
+ } else if comment.Type == models.CommentTypeLabel {
+ if err = comment.LoadLabel(); err != nil {
+ ctx.ServerError("LoadLabel", err)
+ return
+ }
+ } else if comment.Type == models.CommentTypeMilestone {
+ if err = comment.LoadMilestone(); err != nil {
+ ctx.ServerError("LoadMilestone", err)
+ return
+ }
+ ghostMilestone := &models.Milestone{
+ ID: -1,
+ Name: ctx.Tr("repo.issues.deleted_milestone"),
+ }
+ if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
+ comment.OldMilestone = ghostMilestone
+ }
+ if comment.MilestoneID > 0 && comment.Milestone == nil {
+ comment.Milestone = ghostMilestone
+ }
+ } else if comment.Type == models.CommentTypeProject {
+
+ if err = comment.LoadProject(); err != nil {
+ ctx.ServerError("LoadProject", err)
+ return
+ }
+
+ ghostProject := &models.Project{
+ ID: -1,
+ Title: ctx.Tr("repo.issues.deleted_project"),
+ }
+
+ if comment.OldProjectID > 0 && comment.OldProject == nil {
+ comment.OldProject = ghostProject
+ }
+
+ if comment.ProjectID > 0 && comment.Project == nil {
+ comment.Project = ghostProject
+ }
+
+ } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
+ if err = comment.LoadAssigneeUserAndTeam(); err != nil {
+ ctx.ServerError("LoadAssigneeUserAndTeam", err)
+ return
+ }
+ } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
+ if err = comment.LoadDepIssueDetails(); err != nil {
+ if !models.IsErrIssueNotExist(err) {
+ ctx.ServerError("LoadDepIssueDetails", err)
+ return
+ }
+ }
+ } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
+ comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
+ URLPrefix: ctx.Repo.RepoLink,
+ Metas: ctx.Repo.Repository.ComposeMetas(),
+ }, comment.Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+ if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
+ ctx.ServerError("LoadReview", err)
+ return
+ }
+ participants = addParticipant(comment.Poster, participants)
+ if comment.Review == nil {
+ continue
+ }
+ if err = comment.Review.LoadAttributes(); err != nil {
+ if !models.IsErrUserNotExist(err) {
+ ctx.ServerError("Review.LoadAttributes", err)
+ return
+ }
+ comment.Review.Reviewer = models.NewGhostUser()
+ }
+ if err = comment.Review.LoadCodeComments(); err != nil {
+ ctx.ServerError("Review.LoadCodeComments", err)
+ return
+ }
+ for _, codeComments := range comment.Review.CodeComments {
+ for _, lineComments := range codeComments {
+ for _, c := range lineComments {
+ // Check tag.
+ tag, ok = marked[c.PosterID]
+ if ok {
+ c.ShowTag = tag
+ continue
+ }
+
+ c.ShowTag, err = commentTag(repo, c.Poster, issue)
+ if err != nil {
+ ctx.ServerError("commentTag", err)
+ return
+ }
+ marked[c.PosterID] = c.ShowTag
+ participants = addParticipant(c.Poster, participants)
+ }
+ }
+ }
+ if err = comment.LoadResolveDoer(); err != nil {
+ ctx.ServerError("LoadResolveDoer", err)
+ return
+ }
+ } else if comment.Type == models.CommentTypePullPush {
+ participants = addParticipant(comment.Poster, participants)
+ if err = comment.LoadPushCommits(); err != nil {
+ ctx.ServerError("LoadPushCommits", err)
+ return
+ }
+ } else if comment.Type == models.CommentTypeAddTimeManual ||
+ comment.Type == models.CommentTypeStopTracking {
+ // drop error since times could be pruned from DB..
+ _ = comment.LoadTime()
+ }
+ }
+
+ // Combine multiple label assignments into a single comment
+ combineLabelComments(issue)
+
+ getBranchData(ctx, issue)
+ if issue.IsPull {
+ pull := issue.PullRequest
+ pull.Issue = issue
+ canDelete := false
+ ctx.Data["AllowMerge"] = false
+
+ if ctx.IsSigned {
+ if err := pull.LoadHeadRepo(); err != nil {
+ log.Error("LoadHeadRepo: %v", err)
+ } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
+ perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+ if perm.CanWrite(models.UnitTypeCode) {
+ // Check if branch is not protected
+ if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
+ log.Error("IsProtectedBranch: %v", err)
+ } else if !protected {
+ canDelete = true
+ ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index) + "/cleanup"
+ }
+ }
+ }
+
+ if err := pull.LoadBaseRepo(); err != nil {
+ log.Error("LoadBaseRepo: %v", err)
+ }
+ perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+ ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
+ if err != nil {
+ ctx.ServerError("IsUserAllowedToMerge", err)
+ return
+ }
+
+ if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
+ ctx.ServerError("CanMarkConversation", err)
+ return
+ }
+ }
+
+ prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
+ if err != nil {
+ ctx.ServerError("GetUnit", err)
+ return
+ }
+ prConfig := prUnit.PullRequestsConfig()
+
+ // Check correct values and select default
+ if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
+ !prConfig.IsMergeStyleAllowed(ms) {
+ defaultMergeStyle := prConfig.GetDefaultMergeStyle()
+ if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
+ ctx.Data["MergeStyle"] = defaultMergeStyle
+ } else if prConfig.AllowMerge {
+ ctx.Data["MergeStyle"] = models.MergeStyleMerge
+ } else if prConfig.AllowRebase {
+ ctx.Data["MergeStyle"] = models.MergeStyleRebase
+ } else if prConfig.AllowRebaseMerge {
+ ctx.Data["MergeStyle"] = models.MergeStyleRebaseMerge
+ } else if prConfig.AllowSquash {
+ ctx.Data["MergeStyle"] = models.MergeStyleSquash
+ } else if prConfig.AllowManualMerge {
+ ctx.Data["MergeStyle"] = models.MergeStyleManuallyMerged
+ } else {
+ ctx.Data["MergeStyle"] = ""
+ }
+ }
+ if err = pull.LoadProtectedBranch(); err != nil {
+ ctx.ServerError("LoadProtectedBranch", err)
+ return
+ }
+ if pull.ProtectedBranch != nil {
+ cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
+ ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
+ ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
+ ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
+ ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
+ ctx.Data["GrantedApprovals"] = cnt
+ ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
+ ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
+ ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
+ ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
+ }
+ ctx.Data["WillSign"] = false
+ if ctx.User != nil {
+ sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
+ ctx.Data["WillSign"] = sign
+ ctx.Data["SigningKey"] = key
+ if err != nil {
+ if models.IsErrWontSign(err) {
+ ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason
+ } else {
+ ctx.Data["WontSignReason"] = "error"
+ log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
+ }
+ }
+ } else {
+ ctx.Data["WontSignReason"] = "not_signed_in"
+ }
+ ctx.Data["IsPullBranchDeletable"] = canDelete &&
+ pull.HeadRepo != nil &&
+ git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
+ (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
+
+ stillCanManualMerge := func() bool {
+ if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
+ return false
+ }
+ if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
+ return false
+ }
+ if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
+ return true
+ }
+
+ return false
+ }
+
+ ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
+ }
+
+ // Get Dependencies
+ ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
+ if err != nil {
+ ctx.ServerError("BlockedByDependencies", err)
+ return
+ }
+ ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
+ if err != nil {
+ ctx.ServerError("BlockingDependencies", err)
+ return
+ }
+
+ ctx.Data["Participants"] = participants
+ ctx.Data["NumParticipants"] = len(participants)
+ ctx.Data["Issue"] = issue
+ ctx.Data["ReadOnly"] = false
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
+ ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(models.UnitTypeProjects)
+ ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
+ ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
+ ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
+ ctx.HTML(http.StatusOK, tplIssueView)
+}
+
+// GetActionIssue will return the issue which is used in the context.
+func GetActionIssue(ctx *context.Context) *models.Issue {
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
+ return nil
+ }
+ issue.Repo = ctx.Repo.Repository
+ checkIssueRights(ctx, issue)
+ if ctx.Written() {
+ return nil
+ }
+ if err = issue.LoadAttributes(); err != nil {
+ ctx.ServerError("LoadAttributes", nil)
+ return nil
+ }
+ return issue
+}
+
+func checkIssueRights(ctx *context.Context, issue *models.Issue) {
+ if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) ||
+ !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
+ ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+ }
+}
+
+func getActionIssues(ctx *context.Context) []*models.Issue {
+ commaSeparatedIssueIDs := ctx.Query("issue_ids")
+ if len(commaSeparatedIssueIDs) == 0 {
+ return nil
+ }
+ issueIDs := make([]int64, 0, 10)
+ for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+ issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
+ if err != nil {
+ ctx.ServerError("ParseInt", err)
+ return nil
+ }
+ issueIDs = append(issueIDs, issueID)
+ }
+ issues, err := models.GetIssuesByIDs(issueIDs)
+ if err != nil {
+ ctx.ServerError("GetIssuesByIDs", err)
+ return nil
+ }
+ // Check access rights for all issues
+ issueUnitEnabled := ctx.Repo.CanRead(models.UnitTypeIssues)
+ prUnitEnabled := ctx.Repo.CanRead(models.UnitTypePullRequests)
+ for _, issue := range issues {
+ if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
+ ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+ return nil
+ }
+ if err = issue.LoadAttributes(); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return nil
+ }
+ }
+ return issues
+}
+
+// UpdateIssueTitle change issue's title
+func UpdateIssueTitle(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ title := ctx.QueryTrim("title")
+ if len(title) == 0 {
+ ctx.Error(http.StatusNoContent)
+ return
+ }
+
+ if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil {
+ ctx.ServerError("ChangeTitle", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "title": issue.Title,
+ })
+}
+
+// UpdateIssueRef change issue's ref (branch)
+func UpdateIssueRef(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ ref := ctx.QueryTrim("ref")
+
+ if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil {
+ ctx.ServerError("ChangeRef", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ref": ref,
+ })
+}
+
+// UpdateIssueContent change issue's content
+func UpdateIssueContent(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ content := ctx.Query("content")
+ if err := issue_service.ChangeContent(issue, ctx.User, content); err != nil {
+ ctx.ServerError("ChangeContent", err)
+ return
+ }
+
+ files := ctx.QueryStrings("files[]")
+ if err := updateAttachments(issue, files); err != nil {
+ ctx.ServerError("UpdateAttachments", err)
+ return
+ }
+
+ content, err := markdown.RenderString(&markup.RenderContext{
+ URLPrefix: ctx.Query("context"),
+ Metas: ctx.Repo.Repository.ComposeMetas(),
+ }, issue.Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "content": content,
+ "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
+ })
+}
+
+// UpdateIssueMilestone change issue's milestone
+func UpdateIssueMilestone(ctx *context.Context) {
+ issues := getActionIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ milestoneID := ctx.QueryInt64("id")
+ for _, issue := range issues {
+ oldMilestoneID := issue.MilestoneID
+ if oldMilestoneID == milestoneID {
+ continue
+ }
+ issue.MilestoneID = milestoneID
+ if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
+ ctx.ServerError("ChangeMilestoneAssign", err)
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// UpdateIssueAssignee change issue's or pull's assignee
+func UpdateIssueAssignee(ctx *context.Context) {
+ issues := getActionIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ assigneeID := ctx.QueryInt64("id")
+ action := ctx.Query("action")
+
+ for _, issue := range issues {
+ switch action {
+ case "clear":
+ if err := issue_service.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
+ ctx.ServerError("ClearAssignees", err)
+ return
+ }
+ default:
+ assignee, err := models.GetUserByID(assigneeID)
+ if err != nil {
+ ctx.ServerError("GetUserByID", err)
+ return
+ }
+
+ valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
+ if err != nil {
+ ctx.ServerError("canBeAssigned", err)
+ return
+ }
+ if !valid {
+ ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
+ return
+ }
+
+ _, _, err = issue_service.ToggleAssignee(issue, ctx.User, assigneeID)
+ if err != nil {
+ ctx.ServerError("ToggleAssignee", err)
+ return
+ }
+ }
+ }
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// UpdatePullReviewRequest add or remove review request
+func UpdatePullReviewRequest(ctx *context.Context) {
+ issues := getActionIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ reviewID := ctx.QueryInt64("id")
+ action := ctx.Query("action")
+
+ // TODO: Not support 'clear' now
+ if action != "attach" && action != "detach" {
+ ctx.Status(403)
+ return
+ }
+
+ for _, issue := range issues {
+ if err := issue.LoadRepo(); err != nil {
+ ctx.ServerError("issue.LoadRepo", err)
+ return
+ }
+
+ if !issue.IsPull {
+ log.Warn(
+ "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
+ issue.Repo, issue.Index,
+ )
+ ctx.Status(403)
+ return
+ }
+ if reviewID < 0 {
+ // negative reviewIDs represent team requests
+ if err := issue.Repo.GetOwner(); err != nil {
+ ctx.ServerError("issue.Repo.GetOwner", err)
+ return
+ }
+
+ if !issue.Repo.Owner.IsOrganization() {
+ log.Warn(
+ "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
+ issue.Repo.FullName(), issue.Index, issue.Repo.ID,
+ )
+ ctx.Status(403)
+ return
+ }
+
+ team, err := models.GetTeamByID(-reviewID)
+ if err != nil {
+ ctx.ServerError("models.GetTeamByID", err)
+ return
+ }
+
+ if team.OrgID != issue.Repo.OwnerID {
+ log.Warn(
+ "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
+ team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
+ ctx.Status(403)
+ return
+ }
+
+ err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue)
+ if err != nil {
+ if models.IsErrNotValidReviewRequest(err) {
+ log.Warn(
+ "UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
+ team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
+ err,
+ )
+ ctx.Status(403)
+ return
+ }
+ ctx.ServerError("IsValidTeamReviewRequest", err)
+ return
+ }
+
+ _, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach")
+ if err != nil {
+ ctx.ServerError("TeamReviewRequest", err)
+ return
+ }
+ continue
+ }
+
+ reviewer, err := models.GetUserByID(reviewID)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ log.Warn(
+ "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
+ reviewID, issue.Repo, issue.Index,
+ err,
+ )
+ ctx.Status(403)
+ return
+ }
+ ctx.ServerError("GetUserByID", err)
+ return
+ }
+
+ err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil)
+ if err != nil {
+ if models.IsErrNotValidReviewRequest(err) {
+ log.Warn(
+ "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
+ reviewer, issue.Repo, issue.Index,
+ err,
+ )
+ ctx.Status(403)
+ return
+ }
+ ctx.ServerError("isValidReviewRequest", err)
+ return
+ }
+
+ _, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach")
+ if err != nil {
+ ctx.ServerError("ReviewRequest", err)
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// UpdateIssueStatus change issue's status
+func UpdateIssueStatus(ctx *context.Context) {
+ issues := getActionIssues(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ var isClosed bool
+ switch action := ctx.Query("action"); action {
+ case "open":
+ isClosed = false
+ case "close":
+ isClosed = true
+ default:
+ log.Warn("Unrecognized action: %s", action)
+ }
+
+ if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
+ ctx.ServerError("LoadRepositories", err)
+ return
+ }
+ for _, issue := range issues {
+ if issue.IsClosed != isClosed {
+ if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
+ if models.IsErrDependenciesLeft(err) {
+ ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
+ "error": "cannot close this issue because it still has open dependencies",
+ })
+ return
+ }
+ ctx.ServerError("ChangeStatus", err)
+ return
+ }
+ }
+ }
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": true,
+ })
+}
+
+// NewComment create a comment for issue
+func NewComment(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateCommentForm)
+ 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
+ }
+
+ if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
+ ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
+ ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
+ return
+ }
+
+ var attachments []string
+ if setting.Attachment.Enabled {
+ attachments = form.Files
+ }
+
+ if ctx.HasError() {
+ ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+ ctx.Redirect(issue.HTMLURL())
+ return
+ }
+
+ var comment *models.Comment
+ defer func() {
+ // Check if issue admin/poster changes the status of issue.
+ if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
+ (form.Status == "reopen" || form.Status == "close") &&
+ !(issue.IsPull && issue.PullRequest.HasMerged) {
+
+ // Duplication and conflict check should apply to reopen pull request.
+ var pr *models.PullRequest
+
+ if form.Status == "reopen" && issue.IsPull {
+ pull := issue.PullRequest
+ var err error
+ pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
+ if err != nil {
+ if !models.IsErrPullRequestNotExist(err) {
+ ctx.ServerError("GetUnmergedPullRequest", err)
+ return
+ }
+ }
+
+ // Regenerate patch and test conflict.
+ if pr == nil {
+ pull_service.AddToTaskQueue(issue.PullRequest)
+ }
+ }
+
+ if pr != nil {
+ ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
+ } else {
+ isClosed := form.Status == "close"
+ if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
+ log.Error("ChangeStatus: %v", err)
+
+ if models.IsErrDependenciesLeft(err) {
+ if issue.IsPull {
+ ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
+ ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
+ } else {
+ ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
+ ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
+ }
+ return
+ }
+ } else {
+ if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
+ ctx.ServerError("CreateOrStopIssueStopwatch", err)
+ return
+ }
+
+ log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
+ }
+ }
+ }
+
+ // Redirect to comment hashtag if there is any actual content.
+ typeName := "issues"
+ if issue.IsPull {
+ typeName = "pulls"
+ }
+ if comment != nil {
+ ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
+ } else {
+ ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
+ }
+ }()
+
+ // Fix #321: Allow empty comments, as long as we have attachments.
+ if len(form.Content) == 0 && len(attachments) == 0 {
+ return
+ }
+
+ comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
+ if err != nil {
+ ctx.ServerError("CreateIssueComment", err)
+ return
+ }
+
+ log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
+}
+
+// UpdateCommentContent change comment of issue's content
+func UpdateCommentContent(ctx *context.Context) {
+ comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ return
+ }
+
+ if err := comment.LoadIssue(); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ return
+ }
+
+ if comment.Type == models.CommentTypeComment {
+ if err := comment.LoadAttachments(); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+ }
+
+ if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
+ ctx.Error(http.StatusForbidden)
+ return
+ } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+ ctx.Error(http.StatusNoContent)
+ return
+ }
+
+ oldContent := comment.Content
+ comment.Content = ctx.Query("content")
+ if len(comment.Content) == 0 {
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "content": "",
+ })
+ return
+ }
+ if err = comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
+ ctx.ServerError("UpdateComment", err)
+ return
+ }
+
+ files := ctx.QueryStrings("files[]")
+ if err := updateAttachments(comment, files); err != nil {
+ ctx.ServerError("UpdateAttachments", err)
+ return
+ }
+
+ content, err := markdown.RenderString(&markup.RenderContext{
+ URLPrefix: ctx.Query("context"),
+ Metas: ctx.Repo.Repository.ComposeMetas(),
+ }, comment.Content)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "content": content,
+ "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
+ })
+}
+
+// DeleteComment delete comment of issue
+func DeleteComment(ctx *context.Context) {
+ comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ return
+ }
+
+ if err := comment.LoadIssue(); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ return
+ }
+
+ if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
+ ctx.Error(http.StatusForbidden)
+ return
+ } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+ ctx.Error(http.StatusNoContent)
+ return
+ }
+
+ if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
+ ctx.ServerError("DeleteCommentByID", err)
+ return
+ }
+
+ ctx.Status(200)
+}
+
+// ChangeIssueReaction create a reaction for issue
+func ChangeIssueReaction(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.ReactionForm)
+ 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
+ }
+
+ if ctx.HasError() {
+ ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
+ return
+ }
+
+ switch ctx.Params(":action") {
+ case "react":
+ reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
+ if err != nil {
+ if models.IsErrForbiddenIssueReaction(err) {
+ ctx.ServerError("ChangeIssueReaction", err)
+ return
+ }
+ log.Info("CreateIssueReaction: %s", err)
+ break
+ }
+ // Reload new reactions
+ issue.Reactions = nil
+ if err = issue.LoadAttributes(); err != nil {
+ log.Info("issue.LoadAttributes: %s", err)
+ break
+ }
+
+ log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
+ case "unreact":
+ if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
+ ctx.ServerError("DeleteIssueReaction", err)
+ return
+ }
+
+ // Reload new reactions
+ issue.Reactions = nil
+ if err := issue.LoadAttributes(); err != nil {
+ log.Info("issue.LoadAttributes: %s", err)
+ break
+ }
+
+ log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
+ default:
+ ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
+ return
+ }
+
+ if len(issue.Reactions) == 0 {
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "empty": true,
+ "html": "",
+ })
+ return
+ }
+
+ html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
+ "ctx": ctx.Data,
+ "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
+ "Reactions": issue.Reactions.GroupByType(),
+ })
+ if err != nil {
+ ctx.ServerError("ChangeIssueReaction.HTMLString", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "html": html,
+ })
+}
+
+// ChangeCommentReaction create a reaction for comment
+func ChangeCommentReaction(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.ReactionForm)
+ comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ return
+ }
+
+ if err := comment.LoadIssue(); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ return
+ }
+
+ if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
+ if log.IsTrace() {
+ if ctx.IsSigned {
+ issueType := "issues"
+ if comment.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(comment.Issue.PosterID),
+ issueType,
+ ctx.Repo.Repository,
+ ctx.Repo.Permission)
+ } else {
+ log.Trace("Permission Denied: Not logged in")
+ }
+ }
+
+ ctx.Error(http.StatusForbidden)
+ return
+ } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+ ctx.Error(http.StatusNoContent)
+ return
+ }
+
+ switch ctx.Params(":action") {
+ case "react":
+ reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
+ if err != nil {
+ if models.IsErrForbiddenIssueReaction(err) {
+ ctx.ServerError("ChangeIssueReaction", err)
+ return
+ }
+ log.Info("CreateCommentReaction: %s", err)
+ break
+ }
+ // Reload new reactions
+ comment.Reactions = nil
+ if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
+ log.Info("comment.LoadReactions: %s", err)
+ break
+ }
+
+ log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
+ case "unreact":
+ if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
+ ctx.ServerError("DeleteCommentReaction", err)
+ return
+ }
+
+ // Reload new reactions
+ comment.Reactions = nil
+ if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
+ log.Info("comment.LoadReactions: %s", err)
+ break
+ }
+
+ log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
+ default:
+ ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
+ return
+ }
+
+ if len(comment.Reactions) == 0 {
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "empty": true,
+ "html": "",
+ })
+ return
+ }
+
+ html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
+ "ctx": ctx.Data,
+ "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
+ "Reactions": comment.Reactions.GroupByType(),
+ })
+ if err != nil {
+ ctx.ServerError("ChangeCommentReaction.HTMLString", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "html": html,
+ })
+}
+
+func addParticipant(poster *models.User, participants []*models.User) []*models.User {
+ for _, part := range participants {
+ if poster.ID == part.ID {
+ return participants
+ }
+ }
+ return append(participants, poster)
+}
+
+func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
+ // Remove comments that the user has no permissions to see
+ for i := 0; i < len(issue.Comments); {
+ c := issue.Comments[i]
+ if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
+ var err error
+ // Set RefRepo for description in template
+ c.RefRepo, err = models.GetRepositoryByID(c.RefRepoID)
+ if err != nil {
+ return err
+ }
+ perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.User)
+ if err != nil {
+ return err
+ }
+ if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
+ issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
+ continue
+ }
+ }
+ i++
+ }
+ return nil
+}
+
+// GetIssueAttachments returns attachments for the issue
+func GetIssueAttachments(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ var attachments = make([]*api.Attachment, len(issue.Attachments))
+ for i := 0; i < len(issue.Attachments); i++ {
+ attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
+ }
+ ctx.JSON(http.StatusOK, attachments)
+}
+
+// GetCommentAttachments returns attachments for the comment
+func GetCommentAttachments(ctx *context.Context) {
+ comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ return
+ }
+ var attachments = make([]*api.Attachment, 0)
+ if comment.Type == models.CommentTypeComment {
+ if err := comment.LoadAttachments(); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+ for i := 0; i < len(comment.Attachments); i++ {
+ attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
+ }
+ }
+ ctx.JSON(http.StatusOK, attachments)
+}
+
+func updateAttachments(item interface{}, files []string) error {
+ var attachments []*models.Attachment
+ switch content := item.(type) {
+ case *models.Issue:
+ attachments = content.Attachments
+ case *models.Comment:
+ attachments = content.Attachments
+ default:
+ return fmt.Errorf("Unknown Type: %T", content)
+ }
+ for i := 0; i < len(attachments); i++ {
+ if util.IsStringInSlice(attachments[i].UUID, files) {
+ continue
+ }
+ if err := models.DeleteAttachment(attachments[i], true); err != nil {
+ return err
+ }
+ }
+ var err error
+ if len(files) > 0 {
+ switch content := item.(type) {
+ case *models.Issue:
+ err = content.UpdateAttachments(files)
+ case *models.Comment:
+ err = content.UpdateAttachments(files)
+ default:
+ return fmt.Errorf("Unknown Type: %T", content)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ switch content := item.(type) {
+ case *models.Issue:
+ content.Attachments, err = models.GetAttachmentsByIssueID(content.ID)
+ case *models.Comment:
+ content.Attachments, err = models.GetAttachmentsByCommentID(content.ID)
+ default:
+ return fmt.Errorf("Unknown Type: %T", content)
+ }
+ return err
+}
+
+func attachmentsHTML(ctx *context.Context, attachments []*models.Attachment, content string) string {
+ attachHTML, err := ctx.HTMLString(string(tplAttachment), map[string]interface{}{
+ "ctx": ctx.Data,
+ "Attachments": attachments,
+ "Content": content,
+ })
+ if err != nil {
+ ctx.ServerError("attachmentsHTML.HTMLString", err)
+ return ""
+ }
+ return attachHTML
+}
+
+// combineLabelComments combine the nearby label comments as one.
+func combineLabelComments(issue *models.Issue) {
+ var prev, cur *models.Comment
+ for i := 0; i < len(issue.Comments); i++ {
+ cur = issue.Comments[i]
+ if i > 0 {
+ prev = issue.Comments[i-1]
+ }
+ if i == 0 || cur.Type != models.CommentTypeLabel ||
+ (prev != nil && prev.PosterID != cur.PosterID) ||
+ (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
+ if cur.Type == models.CommentTypeLabel && cur.Label != nil {
+ if cur.Content != "1" {
+ cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
+ } else {
+ cur.AddedLabels = append(cur.AddedLabels, cur.Label)
+ }
+ }
+ continue
+ }
+
+ if cur.Label != nil { // now cur MUST be label comment
+ if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment
+ if cur.Content != "1" {
+ prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
+ } else {
+ prev.AddedLabels = append(prev.AddedLabels, cur.Label)
+ }
+ prev.CreatedUnix = cur.CreatedUnix
+ // remove the current comment since it has been combined to prev comment
+ issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
+ i--
+ } else { // if prev is not a label comment, start a new group
+ if cur.Content != "1" {
+ cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
+ } else {
+ cur.AddedLabels = append(cur.AddedLabels, cur.Label)
+ }
+ }
+ }
+ }
+}
+
+// get all teams that current user can mention
+func handleTeamMentions(ctx *context.Context) {
+ if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
+ return
+ }
+
+ isAdmin := false
+ var err error
+ // Admin has super access.
+ if ctx.User.IsAdmin {
+ isAdmin = true
+ } else {
+ isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("IsOwnedBy", err)
+ return
+ }
+ }
+
+ if isAdmin {
+ if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
+ ctx.ServerError("GetTeams", err)
+ return
+ }
+ } else {
+ ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetUserTeams", err)
+ return
+ }
+ }
+
+ ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
+ ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
+ ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.RelAvatarLink()
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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{})
+ }
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+ })
+ }
+}
--- /dev/null
+// 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())
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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("..", "..", ".."))
+}
--- /dev/null
+// 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"] = ""
+ }
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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 + "/")
+}
--- /dev/null
+// 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())
+}
--- /dev/null
+// Copyright 2018 The Gitea Authors.
+// 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 (
+ "container/list"
+ "crypto/subtle"
+ "errors"
+ "fmt"
+ "net/http"
+ "path"
+ "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/notification"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/upload"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/gitdiff"
+ pull_service "code.gitea.io/gitea/services/pull"
+ repo_service "code.gitea.io/gitea/services/repository"
+ "github.com/unknwon/com"
+)
+
+const (
+ tplFork base.TplName = "repo/pulls/fork"
+ tplCompareDiff base.TplName = "repo/diff/compare"
+ tplPullCommits base.TplName = "repo/pulls/commits"
+ tplPullFiles base.TplName = "repo/pulls/files"
+
+ pullRequestTemplateKey = "PullRequestTemplate"
+)
+
+var (
+ pullRequestTemplateCandidates = []string{
+ "PULL_REQUEST_TEMPLATE.md",
+ "pull_request_template.md",
+ ".gitea/PULL_REQUEST_TEMPLATE.md",
+ ".gitea/pull_request_template.md",
+ ".github/PULL_REQUEST_TEMPLATE.md",
+ ".github/pull_request_template.md",
+ }
+)
+
+func getRepository(ctx *context.Context, repoID int64) *models.Repository {
+ repo, err := models.GetRepositoryByID(repoID)
+ if err != nil {
+ if models.IsErrRepoNotExist(err) {
+ ctx.NotFound("GetRepositoryByID", nil)
+ } else {
+ ctx.ServerError("GetRepositoryByID", err)
+ }
+ return nil
+ }
+
+ perm, err := models.GetUserRepoPermission(repo, ctx.User)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return nil
+ }
+
+ if !perm.CanRead(models.UnitTypeCode) {
+ log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
+ "User in repo has Permissions: %-+v",
+ ctx.User,
+ models.UnitTypeCode,
+ ctx.Repo,
+ perm)
+ ctx.NotFound("getRepository", nil)
+ return nil
+ }
+ return repo
+}
+
+func getForkRepository(ctx *context.Context) *models.Repository {
+ forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
+ if ctx.Written() {
+ return nil
+ }
+
+ if forkRepo.IsEmpty {
+ log.Trace("Empty repository %-v", forkRepo)
+ ctx.NotFound("getForkRepository", nil)
+ return nil
+ }
+
+ if err := forkRepo.GetOwner(); err != nil {
+ ctx.ServerError("GetOwner", err)
+ return nil
+ }
+
+ ctx.Data["repo_name"] = forkRepo.Name
+ ctx.Data["description"] = forkRepo.Description
+ ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+ canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID)
+
+ ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name
+ ctx.Data["ForkFromOwnerID"] = forkRepo.Owner.ID
+
+ if err := ctx.User.GetOwnedOrganizations(); err != nil {
+ ctx.ServerError("GetOwnedOrganizations", err)
+ return nil
+ }
+ var orgs []*models.User
+ for _, org := range ctx.User.OwnedOrgs {
+ if forkRepo.OwnerID != org.ID && !org.HasForkedRepo(forkRepo.ID) {
+ orgs = append(orgs, org)
+ }
+ }
+
+ var traverseParentRepo = forkRepo
+ var err error
+ for {
+ if ctx.User.ID == traverseParentRepo.OwnerID {
+ canForkToUser = false
+ } else {
+ for i, org := range orgs {
+ if org.ID == traverseParentRepo.OwnerID {
+ orgs = append(orgs[:i], orgs[i+1:]...)
+ break
+ }
+ }
+ }
+
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return nil
+ }
+ }
+
+ ctx.Data["CanForkToUser"] = canForkToUser
+ ctx.Data["Orgs"] = orgs
+
+ if canForkToUser {
+ ctx.Data["ContextUser"] = ctx.User
+ } else if len(orgs) > 0 {
+ ctx.Data["ContextUser"] = orgs[0]
+ }
+
+ return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+
+ getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateRepoForm)
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+
+ ctxUser := checkContextUser(ctx, form.UID)
+ if ctx.Written() {
+ return
+ }
+
+ forkRepo := getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["ContextUser"] = ctxUser
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplFork)
+ return
+ }
+
+ var err error
+ var traverseParentRepo = forkRepo
+ for {
+ if ctxUser.ID == traverseParentRepo.OwnerID {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ return
+ }
+ repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID)
+ if has {
+ ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+ return
+ }
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = models.GetRepositoryByID(traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return
+ }
+ }
+
+ // Check ownership of organization.
+ if ctxUser.IsOrganization() {
+ isOwner, err := ctxUser.IsOwnedBy(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("IsOwnedBy", err)
+ return
+ } else if !isOwner {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ repo, err := repo_service.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description)
+ if err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case models.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ case models.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplFork, &form)
+ case models.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ default:
+ ctx.ServerError("ForkPost", err)
+ }
+ return
+ }
+
+ log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+ ctx.Redirect(ctxUser.HomeLink() + "/" + repo.Name)
+}
+
+func checkPullInfo(ctx *context.Context) *models.Issue {
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.NotFound("GetIssueByIndex", err)
+ } else {
+ ctx.ServerError("GetIssueByIndex", err)
+ }
+ return nil
+ }
+ if err = issue.LoadPoster(); err != nil {
+ ctx.ServerError("LoadPoster", err)
+ return nil
+ }
+ if err := issue.LoadRepo(); err != nil {
+ ctx.ServerError("LoadRepo", err)
+ return nil
+ }
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+ ctx.Data["Issue"] = issue
+
+ if !issue.IsPull {
+ ctx.NotFound("ViewPullCommits", nil)
+ return nil
+ }
+
+ if err = issue.LoadPullRequest(); err != nil {
+ ctx.ServerError("LoadPullRequest", err)
+ return nil
+ }
+
+ if err = issue.PullRequest.LoadHeadRepo(); err != nil {
+ ctx.ServerError("LoadHeadRepo", err)
+ return nil
+ }
+
+ if ctx.IsSigned {
+ // Update issue-user.
+ if err = issue.ReadBy(ctx.User.ID); err != nil {
+ ctx.ServerError("ReadBy", err)
+ return nil
+ }
+ }
+
+ return issue
+}
+
+func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
+ if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
+ ctx.Data["HeadTarget"] = pull.HeadBranch
+ } else if pull.HeadRepo == nil {
+ ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
+ } else {
+ ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
+ }
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
+ ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL()
+}
+
+// PrepareMergedViewPullInfo show meta information for a merged pull request view page
+func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+ pull := issue.PullRequest
+
+ setMergeTarget(ctx, pull)
+ ctx.Data["HasMerged"] = true
+
+ compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
+ pull.MergeBase, pull.GetGitRefName())
+ if err != nil {
+ if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
+ ctx.Data["IsPullRequestBroken"] = true
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["NumCommits"] = 0
+ ctx.Data["NumFiles"] = 0
+ return nil
+ }
+
+ ctx.ServerError("GetCompareInfo", err)
+ return nil
+ }
+ ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+ ctx.Data["NumFiles"] = compareInfo.NumFiles
+
+ if compareInfo.Commits.Len() != 0 {
+ sha := compareInfo.Commits.Front().Value.(*git.Commit).ID.String()
+ commitStatuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, sha, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLatestCommitStatus", err)
+ return nil
+ }
+ if len(commitStatuses) != 0 {
+ ctx.Data["LatestCommitStatuses"] = commitStatuses
+ ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ }
+ }
+
+ return compareInfo
+}
+
+// PrepareViewPullInfo show meta information for a pull request preview page
+func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+ repo := ctx.Repo.Repository
+ pull := issue.PullRequest
+
+ if err := pull.LoadHeadRepo(); err != nil {
+ ctx.ServerError("LoadHeadRepo", err)
+ return nil
+ }
+
+ if err := pull.LoadBaseRepo(); err != nil {
+ ctx.ServerError("LoadBaseRepo", err)
+ return nil
+ }
+
+ setMergeTarget(ctx, pull)
+
+ if err := pull.LoadProtectedBranch(); err != nil {
+ ctx.ServerError("LoadProtectedBranch", err)
+ return nil
+ }
+ ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
+
+ baseGitRepo, err := git.OpenRepository(pull.BaseRepo.RepoPath())
+ if err != nil {
+ ctx.ServerError("OpenRepository", err)
+ return nil
+ }
+ defer baseGitRepo.Close()
+
+ if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
+ ctx.Data["IsPullRequestBroken"] = true
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["HeadTarget"] = pull.HeadBranch
+
+ sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+ if err != nil {
+ ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+ return nil
+ }
+ commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLatestCommitStatus", err)
+ return nil
+ }
+ if len(commitStatuses) > 0 {
+ ctx.Data["LatestCommitStatuses"] = commitStatuses
+ ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ }
+
+ compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
+ pull.MergeBase, pull.GetGitRefName())
+ if err != nil {
+ if strings.Contains(err.Error(), "fatal: Not a valid object name") {
+ ctx.Data["IsPullRequestBroken"] = true
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["NumCommits"] = 0
+ ctx.Data["NumFiles"] = 0
+ return nil
+ }
+
+ ctx.ServerError("GetCompareInfo", err)
+ return nil
+ }
+
+ ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+ ctx.Data["NumFiles"] = compareInfo.NumFiles
+ return compareInfo
+ }
+
+ var headBranchExist bool
+ var headBranchSha string
+ // HeadRepo may be missing
+ if pull.HeadRepo != nil {
+ headGitRepo, err := git.OpenRepository(pull.HeadRepo.RepoPath())
+ if err != nil {
+ ctx.ServerError("OpenRepository", err)
+ return nil
+ }
+ defer headGitRepo.Close()
+
+ headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
+
+ if headBranchExist {
+ headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
+ if err != nil {
+ ctx.ServerError("GetBranchCommitID", err)
+ return nil
+ }
+ }
+ }
+
+ if headBranchExist {
+ ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
+ if err != nil {
+ ctx.ServerError("IsUserAllowedToUpdate", err)
+ return nil
+ }
+ ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(pull)
+ }
+
+ sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.Data["IsPullRequestBroken"] = true
+ if pull.IsSameRepo() {
+ ctx.Data["HeadTarget"] = pull.HeadBranch
+ } else if pull.HeadRepo == nil {
+ ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
+ } else {
+ ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
+ }
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["NumCommits"] = 0
+ ctx.Data["NumFiles"] = 0
+ return nil
+ }
+ ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
+ return nil
+ }
+
+ commitStatuses, err := models.GetLatestCommitStatus(repo.ID, sha, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLatestCommitStatus", err)
+ return nil
+ }
+ if len(commitStatuses) > 0 {
+ ctx.Data["LatestCommitStatuses"] = commitStatuses
+ ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ }
+
+ if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
+ ctx.Data["is_context_required"] = func(context string) bool {
+ for _, c := range pull.ProtectedBranch.StatusCheckContexts {
+ if c == context {
+ return true
+ }
+ }
+ return false
+ }
+ ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
+ }
+
+ ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
+ ctx.Data["HeadBranchCommitID"] = headBranchSha
+ ctx.Data["PullHeadCommitID"] = sha
+
+ if pull.HeadRepo == nil || !headBranchExist || headBranchSha != sha {
+ ctx.Data["IsPullRequestBroken"] = true
+ if pull.IsSameRepo() {
+ ctx.Data["HeadTarget"] = pull.HeadBranch
+ } else if pull.HeadRepo == nil {
+ ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
+ } else {
+ ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
+ }
+ }
+
+ compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
+ git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName())
+ if err != nil {
+ if strings.Contains(err.Error(), "fatal: Not a valid object name") {
+ ctx.Data["IsPullRequestBroken"] = true
+ ctx.Data["BaseTarget"] = pull.BaseBranch
+ ctx.Data["NumCommits"] = 0
+ ctx.Data["NumFiles"] = 0
+ return nil
+ }
+
+ ctx.ServerError("GetCompareInfo", err)
+ return nil
+ }
+
+ ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+
+ if pull.IsWorkInProgress() {
+ ctx.Data["IsPullWorkInProgress"] = true
+ ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
+ }
+
+ if pull.IsFilesConflicted() {
+ ctx.Data["IsPullFilesConflicted"] = true
+ ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
+ }
+
+ ctx.Data["NumCommits"] = compareInfo.Commits.Len()
+ ctx.Data["NumFiles"] = compareInfo.NumFiles
+ return compareInfo
+}
+
+// ViewPullCommits show commits for a pull request
+func ViewPullCommits(ctx *context.Context) {
+ ctx.Data["PageIsPullList"] = true
+ ctx.Data["PageIsPullCommits"] = true
+
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ pull := issue.PullRequest
+
+ var commits *list.List
+ var prInfo *git.CompareInfo
+ if pull.HasMerged {
+ prInfo = PrepareMergedViewPullInfo(ctx, issue)
+ } else {
+ prInfo = PrepareViewPullInfo(ctx, issue)
+ }
+
+ if ctx.Written() {
+ return
+ } else if prInfo == nil {
+ ctx.NotFound("ViewPullCommits", nil)
+ return
+ }
+
+ ctx.Data["Username"] = ctx.Repo.Owner.Name
+ ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+ commits = prInfo.Commits
+ commits = models.ValidateCommitsWithEmails(commits)
+ commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
+ commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = commits
+ ctx.Data["CommitCount"] = commits.Len()
+
+ getBranchData(ctx, issue)
+ ctx.HTML(http.StatusOK, tplPullCommits)
+}
+
+// ViewPullFiles render pull request changed files list page
+func ViewPullFiles(ctx *context.Context) {
+ ctx.Data["PageIsPullList"] = true
+ ctx.Data["PageIsPullFiles"] = true
+
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ pull := issue.PullRequest
+
+ var (
+ diffRepoPath string
+ startCommitID string
+ endCommitID string
+ gitRepo *git.Repository
+ )
+
+ var prInfo *git.CompareInfo
+ if pull.HasMerged {
+ prInfo = PrepareMergedViewPullInfo(ctx, issue)
+ } else {
+ prInfo = PrepareViewPullInfo(ctx, issue)
+ }
+
+ if ctx.Written() {
+ return
+ } else if prInfo == nil {
+ ctx.NotFound("ViewPullFiles", nil)
+ return
+ }
+
+ diffRepoPath = ctx.Repo.GitRepo.Path
+ gitRepo = ctx.Repo.GitRepo
+
+ headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+ if err != nil {
+ ctx.ServerError("GetRefCommitID", err)
+ return
+ }
+
+ startCommitID = prInfo.MergeBase
+ endCommitID = headCommitID
+
+ ctx.Data["Username"] = ctx.Repo.Owner.Name
+ ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+ ctx.Data["AfterCommitID"] = endCommitID
+
+ diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(diffRepoPath,
+ startCommitID, endCommitID, setting.Git.MaxGitDiffLines,
+ setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
+ gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+ if err != nil {
+ ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
+ return
+ }
+
+ if err = diff.LoadComments(issue, ctx.User); err != nil {
+ ctx.ServerError("LoadComments", err)
+ return
+ }
+
+ if err = pull.LoadProtectedBranch(); err != nil {
+ ctx.ServerError("LoadProtectedBranch", err)
+ return
+ }
+
+ if pull.ProtectedBranch != nil {
+ glob := pull.ProtectedBranch.GetProtectedFilePatterns()
+ if len(glob) != 0 {
+ for _, file := range diff.Files {
+ file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name)
+ }
+ }
+ }
+
+ ctx.Data["Diff"] = diff
+ ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+
+ baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
+ if err != nil {
+ ctx.ServerError("GetCommit", err)
+ return
+ }
+ commit, err := gitRepo.GetCommit(endCommitID)
+ if err != nil {
+ ctx.ServerError("GetCommit", err)
+ return
+ }
+
+ if ctx.IsSigned && ctx.User != nil {
+ if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
+ ctx.ServerError("CanMarkConversation", err)
+ return
+ }
+ }
+
+ headTarget := path.Join(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+ setCompareContext(ctx, baseCommit, commit, headTarget)
+
+ ctx.Data["RequireHighlightJS"] = true
+ ctx.Data["RequireSimpleMDE"] = true
+ ctx.Data["RequireTribute"] = true
+ if ctx.Data["Assignees"], err = ctx.Repo.Repository.GetAssignees(); err != nil {
+ ctx.ServerError("GetAssignees", err)
+ return
+ }
+ handleTeamMentions(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
+ if err != nil && !models.IsErrReviewNotExist(err) {
+ ctx.ServerError("GetCurrentReview", err)
+ return
+ }
+ getBranchData(ctx, issue)
+ ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ ctx.HTML(http.StatusOK, tplPullFiles)
+}
+
+// UpdatePullRequest merge PR's baseBranch into headBranch
+func UpdatePullRequest(ctx *context.Context) {
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ if issue.IsClosed {
+ ctx.NotFound("MergePullRequest", nil)
+ return
+ }
+ if issue.PullRequest.HasMerged {
+ ctx.NotFound("MergePullRequest", nil)
+ return
+ }
+
+ if err := issue.PullRequest.LoadBaseRepo(); err != nil {
+ ctx.ServerError("LoadBaseRepo", err)
+ return
+ }
+ if err := issue.PullRequest.LoadHeadRepo(); err != nil {
+ ctx.ServerError("LoadHeadRepo", err)
+ return
+ }
+
+ allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User)
+ if err != nil {
+ ctx.ServerError("IsUserAllowedToMerge", err)
+ return
+ }
+
+ // ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
+ if !allowedUpdate {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ }
+
+ // default merge commit message
+ message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
+
+ if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil {
+ if models.IsErrMergeConflicts(err) {
+ conflictError := err.(models.ErrMergeConflicts)
+ flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+ "Message": ctx.Tr("repo.pulls.merge_conflict"),
+ "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
+ "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+ })
+ if err != nil {
+ ctx.ServerError("UpdatePullRequest.HTMLString", err)
+ return
+ }
+ ctx.Flash.Error(flashError)
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ }
+ ctx.Flash.Error(err.Error())
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ }
+
+ time.Sleep(1 * time.Second)
+
+ ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+}
+
+// MergePullRequest response for merging pull request
+func MergePullRequest(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.MergePullRequestForm)
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ if issue.IsClosed {
+ if issue.IsPull {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.is_closed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ }
+ ctx.Flash.Error(ctx.Tr("repo.issues.closed_title"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + fmt.Sprint(issue.Index))
+ return
+ }
+
+ pr := issue.PullRequest
+
+ allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
+ if err != nil {
+ ctx.ServerError("IsUserAllowedToMerge", err)
+ return
+ }
+ if !allowedMerge {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(issue.Index))
+ return
+ }
+
+ if pr.HasMerged {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.has_merged"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+ return
+ }
+
+ // handle manually-merged mark
+ if models.MergeStyle(form.Do) == models.MergeStyleManuallyMerged {
+ if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
+ if models.IsErrInvalidMergeStyle(err) {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+ return
+ } else if strings.Contains(err.Error(), "Wrong commit ID") {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+ return
+ }
+
+ ctx.ServerError("MergedManually", err)
+ return
+ }
+
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+ return
+ }
+
+ if !pr.CanAutoMerge() {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
+ return
+ }
+
+ if pr.IsWorkInProgress() {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_wip"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ }
+
+ if err := pull_service.CheckPRReadyToMerge(pr, false); err != nil {
+ if !models.IsErrNotAllowedToMerge(err) {
+ ctx.ServerError("Merge PR status", err)
+ return
+ }
+ if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
+ ctx.ServerError("IsUserRepoAdmin", err)
+ return
+ } else if !isRepoAdmin {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.no_merge_not_ready"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ }
+ }
+
+ if ctx.HasError() {
+ ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ }
+
+ message := strings.TrimSpace(form.MergeTitleField)
+ if len(message) == 0 {
+ if models.MergeStyle(form.Do) == models.MergeStyleMerge {
+ message = pr.GetDefaultMergeMessage()
+ }
+ if models.MergeStyle(form.Do) == models.MergeStyleRebaseMerge {
+ message = pr.GetDefaultMergeMessage()
+ }
+ if models.MergeStyle(form.Do) == models.MergeStyleSquash {
+ message = pr.GetDefaultSquashMessage()
+ }
+ }
+
+ form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
+ if len(form.MergeMessageField) > 0 {
+ message += "\n\n" + form.MergeMessageField
+ }
+
+ pr.Issue = issue
+ pr.Issue.Repo = ctx.Repo.Repository
+
+ noDeps, err := models.IssueNoDependenciesLeft(issue)
+ if err != nil {
+ return
+ }
+
+ if !noDeps {
+ ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ }
+
+ if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
+ if models.IsErrInvalidMergeStyle(err) {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ } else if models.IsErrMergeConflicts(err) {
+ conflictError := err.(models.ErrMergeConflicts)
+ flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+ "Message": ctx.Tr("repo.editor.merge_conflict"),
+ "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
+ "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+ })
+ if err != nil {
+ ctx.ServerError("MergePullRequest.HTMLString", err)
+ return
+ }
+ ctx.Flash.Error(flashError)
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ } else if models.IsErrRebaseConflicts(err) {
+ conflictError := err.(models.ErrRebaseConflicts)
+ flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+ "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
+ "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
+ "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
+ })
+ if err != nil {
+ ctx.ServerError("MergePullRequest.HTMLString", err)
+ return
+ }
+ ctx.Flash.Error(flashError)
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ } else if models.IsErrMergeUnrelatedHistories(err) {
+ log.Debug("MergeUnrelatedHistories error: %v", err)
+ ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ } else if git.IsErrPushOutOfDate(err) {
+ log.Debug("MergePushOutOfDate error: %v", err)
+ ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ } else if git.IsErrPushRejected(err) {
+ log.Debug("MergePushRejected error: %v", err)
+ pushrejErr := err.(*git.ErrPushRejected)
+ message := pushrejErr.Message
+ if len(message) == 0 {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
+ } else {
+ flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+ "Message": ctx.Tr("repo.pulls.push_rejected"),
+ "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
+ "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
+ })
+ if err != nil {
+ ctx.ServerError("MergePullRequest.HTMLString", err)
+ return
+ }
+ ctx.Flash.Error(flashError)
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+ return
+ }
+ ctx.ServerError("Merge", err)
+ return
+ }
+
+ if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
+ ctx.ServerError("CreateOrStopIssueStopwatch", err)
+ return
+ }
+
+ log.Trace("Pull request merged: %d", pr.ID)
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pr.Index))
+}
+
+func stopTimerIfAvailable(user *models.User, issue *models.Issue) error {
+
+ if models.StopwatchExists(user.ID, issue.ID) {
+ if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// CompareAndPullRequestPost response for creating pull request
+func CompareAndPullRequestPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateIssueForm)
+ ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
+ ctx.Data["PageIsComparePull"] = true
+ ctx.Data["IsDiffCompare"] = true
+ ctx.Data["RequireHighlightJS"] = true
+ ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
+ var (
+ repo = ctx.Repo.Repository
+ attachments []string
+ )
+
+ headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ defer headGitRepo.Close()
+
+ labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true)
+ if ctx.Written() {
+ return
+ }
+
+ if setting.Attachment.Enabled {
+ attachments = form.Files
+ }
+
+ if ctx.HasError() {
+ middleware.AssignForm(form, ctx.Data)
+
+ // This stage is already stop creating new pull request, so it does not matter if it has
+ // something to compare or not.
+ PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
+ gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplCompareDiff)
+ return
+ }
+
+ if util.IsEmptyString(form.Title) {
+ PrepareCompareDiff(ctx, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch,
+ gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+ if ctx.Written() {
+ return
+ }
+
+ ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplCompareDiff, form)
+ return
+ }
+
+ pullIssue := &models.Issue{
+ RepoID: repo.ID,
+ Title: form.Title,
+ PosterID: ctx.User.ID,
+ Poster: ctx.User,
+ MilestoneID: milestoneID,
+ IsPull: true,
+ Content: form.Content,
+ }
+ pullRequest := &models.PullRequest{
+ HeadRepoID: headRepo.ID,
+ BaseRepoID: repo.ID,
+ HeadBranch: headBranch,
+ BaseBranch: baseBranch,
+ HeadRepo: headRepo,
+ BaseRepo: repo,
+ MergeBase: prInfo.MergeBase,
+ Type: models.PullRequestGitea,
+ }
+ // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
+ // instead of 500.
+
+ if err := pull_service.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
+ if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+ return
+ } else if git.IsErrPushRejected(err) {
+ pushrejErr := err.(*git.ErrPushRejected)
+ message := pushrejErr.Message
+ if len(message) == 0 {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
+ } else {
+ flashError, err := ctx.HTMLString(string(tplAlertDetails), map[string]interface{}{
+ "Message": ctx.Tr("repo.pulls.push_rejected"),
+ "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
+ "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
+ })
+ if err != nil {
+ ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+ return
+ }
+ ctx.Flash.Error(flashError)
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
+ return
+ }
+ ctx.ServerError("NewPullRequest", err)
+ return
+ }
+
+ log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
+ ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + fmt.Sprint(pullIssue.Index))
+}
+
+// TriggerTask response for a trigger task request
+func TriggerTask(ctx *context.Context) {
+ pusherID := ctx.QueryInt64("pusher")
+ branch := ctx.Query("branch")
+ secret := ctx.Query("secret")
+ if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 {
+ ctx.Error(http.StatusNotFound)
+ log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid")
+ return
+ }
+ owner, repo := parseOwnerAndRepo(ctx)
+ if ctx.Written() {
+ return
+ }
+ got := []byte(base.EncodeMD5(owner.Salt))
+ want := []byte(secret)
+ if subtle.ConstantTimeCompare(got, want) != 1 {
+ ctx.Error(http.StatusNotFound)
+ log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name)
+ return
+ }
+
+ pusher, err := models.GetUserByID(pusherID)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.Error(http.StatusNotFound)
+ } else {
+ ctx.ServerError("GetUserByID", err)
+ }
+ return
+ }
+
+ log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
+
+ go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, "", "")
+ ctx.Status(202)
+}
+
+// CleanUpPullRequest responses for delete merged branch when PR has been merged
+func CleanUpPullRequest(ctx *context.Context) {
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ pr := issue.PullRequest
+
+ // Don't cleanup unmerged and unclosed PRs
+ if !pr.HasMerged && !issue.IsClosed {
+ ctx.NotFound("CleanUpPullRequest", nil)
+ return
+ }
+
+ if err := pr.LoadHeadRepo(); err != nil {
+ ctx.ServerError("LoadHeadRepo", err)
+ return
+ } else if pr.HeadRepo == nil {
+ // Forked repository has already been deleted
+ ctx.NotFound("CleanUpPullRequest", nil)
+ return
+ } else if err = pr.LoadBaseRepo(); err != nil {
+ ctx.ServerError("LoadBaseRepo", err)
+ return
+ } else if err = pr.HeadRepo.GetOwner(); err != nil {
+ ctx.ServerError("HeadRepo.GetOwner", err)
+ return
+ }
+
+ perm, err := models.GetUserRepoPermission(pr.HeadRepo, ctx.User)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+ if !perm.CanWrite(models.UnitTypeCode) {
+ ctx.NotFound("CleanUpPullRequest", nil)
+ return
+ }
+
+ fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
+
+ gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
+ if err != nil {
+ ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
+ return
+ }
+ defer gitRepo.Close()
+
+ gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
+ if err != nil {
+ ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
+ return
+ }
+ defer gitBaseRepo.Close()
+
+ defer func() {
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": pr.BaseRepo.Link() + "/pulls/" + fmt.Sprint(issue.Index),
+ })
+ }()
+
+ // Check if branch has no new commits
+ headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID: %v", err)
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ return
+ }
+ branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
+ if err != nil {
+ log.Error("GetBranchCommitID: %v", err)
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ return
+ }
+ if headCommitID != branchCommitID {
+ ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
+ return
+ }
+
+ if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
+ switch {
+ case git.IsErrBranchNotExist(err):
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ case errors.Is(err, repo_service.ErrBranchIsDefault):
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ case errors.Is(err, repo_service.ErrBranchIsProtected):
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ default:
+ log.Error("DeleteBranch: %v", err)
+ ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
+ }
+ return
+ }
+
+ if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, issue.ID, pr.HeadBranch); err != nil {
+ // Do not fail here as branch has already been deleted
+ log.Error("DeleteBranch: %v", err)
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
+}
+
+// DownloadPullDiff render a pull's raw diff
+func DownloadPullDiff(ctx *context.Context) {
+ DownloadPullDiffOrPatch(ctx, false)
+}
+
+// DownloadPullPatch render a pull's raw patch
+func DownloadPullPatch(ctx *context.Context) {
+ DownloadPullDiffOrPatch(ctx, true)
+}
+
+// DownloadPullDiffOrPatch render a pull's raw diff or patch
+func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.NotFound("GetIssueByIndex", err)
+ } else {
+ ctx.ServerError("GetIssueByIndex", err)
+ }
+ return
+ }
+
+ // Return not found if it's not a pull request
+ if !issue.IsPull {
+ ctx.NotFound("DownloadPullDiff",
+ fmt.Errorf("Issue is not a pull request"))
+ return
+ }
+
+ if err = issue.LoadPullRequest(); err != nil {
+ ctx.ServerError("LoadPullRequest", err)
+ return
+ }
+
+ pr := issue.PullRequest
+
+ if err := pull_service.DownloadDiffOrPatch(pr, ctx, patch); err != nil {
+ ctx.ServerError("DownloadDiffOrPatch", err)
+ return
+ }
+}
+
+// UpdatePullRequestTarget change pull request's target branch
+func UpdatePullRequestTarget(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ pr := issue.PullRequest
+ if ctx.Written() {
+ return
+ }
+ if !issue.IsPull {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ targetBranch := ctx.QueryTrim("target_branch")
+ if len(targetBranch) == 0 {
+ ctx.Error(http.StatusNoContent)
+ return
+ }
+
+ if err := pull_service.ChangeTargetBranch(pr, ctx.User, targetBranch); err != nil {
+ if models.IsErrPullRequestAlreadyExists(err) {
+ err := err.(models.ErrPullRequestAlreadyExists)
+
+ RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
+ errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID)
+
+ ctx.Flash.Error(errorMessage)
+ ctx.JSON(http.StatusConflict, map[string]interface{}{
+ "error": err.Error(),
+ "user_error": errorMessage,
+ })
+ } else if models.IsErrIssueIsClosed(err) {
+ errorMessage := ctx.Tr("repo.pulls.is_closed")
+
+ ctx.Flash.Error(errorMessage)
+ ctx.JSON(http.StatusConflict, map[string]interface{}{
+ "error": err.Error(),
+ "user_error": errorMessage,
+ })
+ } else if models.IsErrPullRequestHasMerged(err) {
+ errorMessage := ctx.Tr("repo.pulls.has_merged")
+
+ ctx.Flash.Error(errorMessage)
+ ctx.JSON(http.StatusConflict, map[string]interface{}{
+ "error": err.Error(),
+ "user_error": errorMessage,
+ })
+ } else if models.IsErrBranchesEqual(err) {
+ errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
+
+ ctx.Flash.Error(errorMessage)
+ ctx.JSON(http.StatusBadRequest, map[string]interface{}{
+ "error": err.Error(),
+ "user_error": errorMessage,
+ })
+ } else {
+ ctx.ServerError("UpdatePullRequestTarget", err)
+ }
+ return
+ }
+ notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, targetBranch)
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "base_branch": pr.BaseBranch,
+ })
+}
--- /dev/null
+// 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()))
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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()
+ }
+}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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"
+ "io/ioutil"
+ "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/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/migrations"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/typesniffer"
+ "code.gitea.io/gitea/modules/validation"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/mailer"
+ mirror_service "code.gitea.io/gitea/services/mirror"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplSettingsOptions base.TplName = "repo/settings/options"
+ tplCollaboration base.TplName = "repo/settings/collaboration"
+ tplBranches base.TplName = "repo/settings/branches"
+ tplGithooks base.TplName = "repo/settings/githooks"
+ tplGithookEdit base.TplName = "repo/settings/githook_edit"
+ tplDeployKeys base.TplName = "repo/settings/deploy_keys"
+ tplProtectedBranch base.TplName = "repo/settings/protected_branch"
+)
+
+// Settings show a repository's settings page
+func Settings(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsOptions"] = true
+ ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
+
+ signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
+ ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningSettings"] = setting.Repository.Signing
+
+ ctx.HTML(http.StatusOK, tplSettingsOptions)
+}
+
+// SettingsPost response for changes of a repository
+func SettingsPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsOptions"] = true
+
+ repo := ctx.Repo.Repository
+
+ switch ctx.Query("action") {
+ case "update":
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplSettingsOptions)
+ return
+ }
+
+ newRepoName := form.RepoName
+ // Check if repository name has been changed.
+ if repo.LowerName != strings.ToLower(newRepoName) {
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
+ if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case models.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
+ case models.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplSettingsOptions, &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"), tplSettingsOptions, form)
+ case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
+ case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
+ default:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
+ }
+ case models.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ default:
+ ctx.ServerError("ChangeRepositoryName", err)
+ }
+ return
+ }
+
+ log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
+ }
+ // In case it's just a case change.
+ repo.Name = newRepoName
+ repo.LowerName = strings.ToLower(newRepoName)
+ repo.Description = form.Description
+ repo.Website = form.Website
+ repo.IsTemplate = form.Template
+
+ // Visibility of forked repository is forced sync with base repository.
+ if repo.IsFork {
+ form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
+ }
+
+ visibilityChanged := repo.IsPrivate != form.Private
+ // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
+ if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.User.IsAdmin {
+ ctx.ServerError("Force Private enabled", errors.New("cannot change private repository to public"))
+ return
+ }
+
+ repo.IsPrivate = form.Private
+ if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
+ ctx.ServerError("UpdateRepository", 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(repo.Link() + "/settings")
+
+ case "mirror":
+ if !repo.IsMirror {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
+
+ interval, err := time.ParseDuration(form.Interval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Data["Err_Interval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ } else {
+ ctx.Repo.Mirror.EnablePrune = form.EnablePrune
+ ctx.Repo.Mirror.Interval = interval
+ if interval != 0 {
+ ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
+ } else {
+ ctx.Repo.Mirror.NextUpdateUnix = 0
+ }
+ if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+ ctx.Data["Err_Interval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ return
+ }
+ }
+
+ oldUsername := mirror_service.Username(ctx.Repo.Mirror)
+ oldPassword := mirror_service.Password(ctx.Repo.Mirror)
+ if form.MirrorPassword == "" && form.MirrorUsername == oldUsername {
+ form.MirrorPassword = oldPassword
+ }
+
+ address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.User)
+ }
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+
+ if err := mirror_service.UpdateAddress(ctx.Repo.Mirror, address); err != nil {
+ ctx.ServerError("UpdateAddress", err)
+ return
+ }
+
+ form.LFS = form.LFS && setting.LFS.StartServer
+
+ if 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"), tplSettingsOptions, &form)
+ return
+ }
+ err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
+ if err != nil {
+ ctx.Data["Err_LFSEndpoint"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+ }
+
+ ctx.Repo.Mirror.LFS = form.LFS
+ ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
+ if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+
+ case "mirror-sync":
+ if !repo.IsMirror {
+ ctx.NotFound("", nil)
+ return
+ }
+
+ mirror_service.StartToMirror(repo.ID)
+
+ ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
+ ctx.Redirect(repo.Link() + "/settings")
+
+ case "advanced":
+ var repoChanged bool
+ var units []models.RepoUnit
+ var deleteUnitTypes []models.UnitType
+
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
+
+ if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
+ repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
+ repoChanged = true
+ }
+
+ if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalWikiURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
+
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypeExternalWiki,
+ Config: &models.ExternalWikiConfig{
+ ExternalWikiURL: form.ExternalWikiURL,
+ },
+ })
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+ } else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypeWiki,
+ Config: new(models.UnitConfig),
+ })
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+ } else {
+ if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+ }
+ if !models.UnitTypeWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+ }
+ }
+
+ if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
+ if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypeExternalTracker,
+ Config: &models.ExternalTrackerConfig{
+ ExternalTrackerURL: form.ExternalTrackerURL,
+ ExternalTrackerFormat: form.TrackerURLFormat,
+ ExternalTrackerStyle: form.TrackerIssueStyle,
+ },
+ })
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+ } else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypeIssues,
+ Config: &models.IssuesConfig{
+ EnableTimetracker: form.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+ EnableDependencies: form.EnableIssueDependencies,
+ },
+ })
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+ } else {
+ if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+ }
+ if !models.UnitTypeIssues.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+ }
+ }
+
+ if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() {
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypeProjects,
+ })
+ } else if !models.UnitTypeProjects.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeProjects)
+ }
+
+ if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
+ units = append(units, models.RepoUnit{
+ RepoID: repo.ID,
+ Type: models.UnitTypePullRequests,
+ Config: &models.PullRequestsConfig{
+ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
+ AllowMerge: form.PullsAllowMerge,
+ AllowRebase: form.PullsAllowRebase,
+ AllowRebaseMerge: form.PullsAllowRebaseMerge,
+ AllowSquash: form.PullsAllowSquash,
+ AllowManualMerge: form.PullsAllowManualMerge,
+ AutodetectManualMerge: form.EnableAutodetectManualMerge,
+ DefaultMergeStyle: models.MergeStyle(form.PullsDefaultMergeStyle),
+ },
+ })
+ } else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
+ }
+
+ if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
+ ctx.ServerError("UpdateRepositoryUnits", err)
+ return
+ }
+ if repoChanged {
+ if err := models.UpdateRepository(repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
+ return
+ }
+ }
+ log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+ case "signing":
+ changed := false
+
+ trustModel := models.ToTrustModel(form.TrustModel)
+ if trustModel != repo.TrustModel {
+ repo.TrustModel = trustModel
+ changed = true
+ }
+
+ if changed {
+ if err := models.UpdateRepository(repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
+ return
+ }
+ }
+ log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+ case "admin":
+ if !ctx.User.IsAdmin {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ if repo.IsFsckEnabled != form.EnableHealthCheck {
+ repo.IsFsckEnabled = form.EnableHealthCheck
+ }
+
+ if err := models.UpdateRepository(repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
+ return
+ }
+
+ log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+ case "convert":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ if !repo.IsMirror {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ repo.IsMirror = false
+
+ if _, err := repository.CleanUpMigrateInfo(repo); err != nil {
+ ctx.ServerError("CleanUpMigrateInfo", err)
+ return
+ } else if err = models.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("DeleteMirrorByRepoID", err)
+ return
+ }
+ log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+ ctx.Redirect(repo.Link())
+
+ case "convert_fork":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if err := repo.GetOwner(); err != nil {
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ if !repo.IsFork {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ if !ctx.Repo.Owner.CanCreateRepo() {
+ ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
+
+ repo.IsFork = false
+ repo.ForkID = 0
+ if err := models.UpdateRepository(repo, false); err != nil {
+ log.Error("Unable to update repository %-v whilst converting from fork", repo)
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
+
+ log.Trace("Repository converted from fork to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
+ ctx.Redirect(repo.Link())
+
+ case "transfer":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
+ return
+ }
+ ctx.ServerError("IsUserExist", err)
+ return
+ }
+
+ if newOwner.Type == models.UserTypeOrganization {
+ if !ctx.User.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !newOwner.HasMemberWithUserID(ctx.User.ID) {
+ // The user shouldn't know about this organization
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
+ return
+ }
+ }
+
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
+
+ if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, repo, nil); err != nil {
+ if models.IsErrRepoAlreadyExist(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
+ } else if models.IsErrRepoTransferInProgress(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+ } else {
+ ctx.ServerError("TransferOwnership", err)
+ }
+
+ return
+ }
+
+ log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
+ ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
+
+ case "cancel_transfer":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+ if err != nil {
+ if models.IsErrNoPendingTransfer(err) {
+ ctx.Flash.Error("repo.settings.transfer_abort_invalid")
+ ctx.Redirect(ctx.User.HomeLink() + "/" + repo.Name + "/settings")
+ } else {
+ ctx.ServerError("GetPendingRepositoryTransfer", err)
+ }
+
+ return
+ }
+
+ if err := repoTransfer.LoadAttributes(); err != nil {
+ ctx.ServerError("LoadRecipient", err)
+ return
+ }
+
+ if err := models.CancelRepositoryTransfer(ctx.Repo.Repository); err != nil {
+ ctx.ServerError("CancelRepositoryTransfer", err)
+ return
+ }
+
+ log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
+ ctx.Redirect(ctx.Repo.Owner.HomeLink() + "/" + repo.Name + "/settings")
+
+ case "delete":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ // Close the gitrepository before doing this.
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ }
+
+ if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
+ ctx.ServerError("DeleteRepository", err)
+ return
+ }
+ log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+ ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+
+ case "delete-wiki":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ err := repo.DeleteWiki()
+ if err != nil {
+ log.Error("Delete Wiki: %v", err.Error())
+ }
+ log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+ case "archive":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ if repo.IsMirror {
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
+
+ if err := repo.SetArchiveRepoState(true); err != nil {
+ log.Error("Tried to archive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
+
+ log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ case "unarchive":
+ if !ctx.Repo.IsOwner() {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ if err := repo.SetArchiveRepoState(false); err != nil {
+ log.Error("Tried to unarchive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
+
+ log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+
+ default:
+ ctx.NotFound("", nil)
+ }
+}
+
+func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
+ if models.IsErrInvalidCloneAddr(err) {
+ addrErr := err.(*models.ErrInvalidCloneAddr)
+ switch {
+ case addrErr.IsProtocolInvalid:
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
+ case addrErr.IsURLError:
+ ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
+ case addrErr.IsPermissionDenied:
+ if addrErr.LocalPath {
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
+ } else if len(addrErr.PrivateNet) == 0 {
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
+ } else {
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_private_ip"), tplSettingsOptions, form)
+ }
+ case addrErr.IsInvalidPath:
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
+ default:
+ ctx.ServerError("Unknown error", err)
+ }
+ }
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
+}
+
+// Collaboration render a repository's collaboration page
+func Collaboration(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsCollaboration"] = true
+
+ users, err := ctx.Repo.Repository.GetCollaborators(models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetCollaborators", err)
+ return
+ }
+ ctx.Data["Collaborators"] = users
+
+ teams, err := ctx.Repo.Repository.GetRepoTeams()
+ if err != nil {
+ ctx.ServerError("GetRepoTeams", err)
+ return
+ }
+ ctx.Data["Teams"] = teams
+ ctx.Data["Repo"] = ctx.Repo.Repository
+ ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID
+ ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName
+ ctx.Data["Org"] = ctx.Repo.Repository.Owner
+ ctx.Data["Units"] = models.Units
+
+ ctx.HTML(http.StatusOK, tplCollaboration)
+}
+
+// CollaborationPost response for actions for a collaboration of a repository
+func CollaborationPost(ctx *context.Context) {
+ name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("collaborator")))
+ if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+ return
+ }
+
+ u, err := models.GetUserByName(name)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+ } else {
+ ctx.ServerError("GetUserByName", err)
+ }
+ return
+ }
+
+ if !u.IsActive {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+ return
+ }
+
+ // Organization is not allowed to be added as a collaborator.
+ if u.IsOrganization() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+ return
+ }
+
+ if got, err := ctx.Repo.Repository.IsCollaborator(u.ID); err == nil && got {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ if err = ctx.Repo.Repository.AddCollaborator(u); err != nil {
+ ctx.ServerError("AddCollaborator", err)
+ return
+ }
+
+ if setting.Service.EnableNotifyMail {
+ mailer.SendCollaboratorMail(u, ctx.User, ctx.Repo.Repository)
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path)
+}
+
+// ChangeCollaborationAccessMode response for changing access of a collaboration
+func ChangeCollaborationAccessMode(ctx *context.Context) {
+ if err := ctx.Repo.Repository.ChangeCollaborationAccessMode(
+ ctx.QueryInt64("uid"),
+ models.AccessMode(ctx.QueryInt("mode"))); err != nil {
+ log.Error("ChangeCollaborationAccessMode: %v", err)
+ }
+}
+
+// DeleteCollaboration delete a collaboration for a repository
+func DeleteCollaboration(ctx *context.Context) {
+ if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
+ })
+}
+
+// AddTeamPost response for adding a team to a repository
+func AddTeamPost(ctx *context.Context) {
+ if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team")))
+ if len(name) == 0 {
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ team, err := ctx.Repo.Owner.GetTeam(name)
+ if err != nil {
+ if models.IsErrTeamNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ } else {
+ ctx.ServerError("GetTeam", err)
+ }
+ return
+ }
+
+ if team.OrgID != ctx.Repo.Repository.OwnerID {
+ ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ if err = team.AddRepository(ctx.Repo.Repository); err != nil {
+ ctx.ServerError("team.AddRepository", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+}
+
+// DeleteTeam response for deleting a team from a repository
+func DeleteTeam(ctx *context.Context) {
+ if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ return
+ }
+
+ team, err := models.GetTeamByID(ctx.QueryInt64("id"))
+ if err != nil {
+ ctx.ServerError("GetTeamByID", err)
+ return
+ }
+
+ if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("team.RemoveRepositorys", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success"))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/collaboration",
+ })
+}
+
+// parseOwnerAndRepo get repos by owner
+func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) {
+ owner, err := models.GetUserByName(ctx.Params(":username"))
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.NotFound("GetUserByName", err)
+ } else {
+ ctx.ServerError("GetUserByName", err)
+ }
+ return nil, nil
+ }
+
+ repo, err := models.GetRepositoryByName(owner.ID, ctx.Params(":reponame"))
+ if err != nil {
+ if models.IsErrRepoNotExist(err) {
+ ctx.NotFound("GetRepositoryByName", err)
+ } else {
+ ctx.ServerError("GetRepositoryByName", err)
+ }
+ return nil, nil
+ }
+
+ return owner, repo
+}
+
+// GitHooks hooks of a repository
+func GitHooks(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
+ ctx.Data["PageIsSettingsGitHooks"] = true
+
+ hooks, err := ctx.Repo.GitRepo.Hooks()
+ if err != nil {
+ ctx.ServerError("Hooks", err)
+ return
+ }
+ ctx.Data["Hooks"] = hooks
+
+ ctx.HTML(http.StatusOK, tplGithooks)
+}
+
+// GitHooksEdit render for editing a hook of repository page
+func GitHooksEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.githooks")
+ ctx.Data["PageIsSettingsGitHooks"] = true
+
+ name := ctx.Params(":name")
+ hook, err := ctx.Repo.GitRepo.GetHook(name)
+ if err != nil {
+ if err == git.ErrNotValidHook {
+ ctx.NotFound("GetHook", err)
+ } else {
+ ctx.ServerError("GetHook", err)
+ }
+ return
+ }
+ ctx.Data["Hook"] = hook
+ ctx.HTML(http.StatusOK, tplGithookEdit)
+}
+
+// GitHooksEditPost response for editing a git hook of a repository
+func GitHooksEditPost(ctx *context.Context) {
+ name := ctx.Params(":name")
+ hook, err := ctx.Repo.GitRepo.GetHook(name)
+ if err != nil {
+ if err == git.ErrNotValidHook {
+ ctx.NotFound("GetHook", err)
+ } else {
+ ctx.ServerError("GetHook", err)
+ }
+ return
+ }
+ hook.Content = ctx.Query("content")
+ if err = hook.Update(); err != nil {
+ ctx.ServerError("hook.Update", err)
+ return
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
+}
+
+// DeployKeys render the deploy keys list of a repository page
+func DeployKeys(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
+ ctx.Data["PageIsSettingsKeys"] = true
+ ctx.Data["DisableSSH"] = setting.SSH.Disabled
+
+ keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("ListDeployKeys", err)
+ return
+ }
+ ctx.Data["Deploykeys"] = keys
+
+ ctx.HTML(http.StatusOK, tplDeployKeys)
+}
+
+// DeployKeysPost response for adding a deploy key of a repository
+func DeployKeysPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AddKeyForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
+ ctx.Data["PageIsSettingsKeys"] = true
+
+ keys, err := models.ListDeployKeys(ctx.Repo.Repository.ID, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("ListDeployKeys", err)
+ return
+ }
+ ctx.Data["Deploykeys"] = keys
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplDeployKeys)
+ return
+ }
+
+ 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.Data["HasError"] = true
+ ctx.Data["Err_Content"] = true
+ ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+ return
+ }
+
+ key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
+ if err != nil {
+ ctx.Data["HasError"] = true
+ switch {
+ case models.IsErrDeployKeyAlreadyExist(err):
+ ctx.Data["Err_Content"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
+ case models.IsErrKeyAlreadyExist(err):
+ ctx.Data["Err_Content"] = true
+ ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
+ case models.IsErrKeyNameAlreadyUsed(err):
+ ctx.Data["Err_Title"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
+ case models.IsErrDeployKeyNameAlreadyUsed(err):
+ ctx.Data["Err_Title"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
+ default:
+ ctx.ServerError("AddDeployKey", err)
+ }
+ return
+ }
+
+ log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID)
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+}
+
+// DeleteDeployKey response for deleting a deploy key
+func DeleteDeployKey(ctx *context.Context) {
+ if err := models.DeleteDeployKey(ctx.User, ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeleteDeployKey: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/keys",
+ })
+}
+
+// UpdateAvatarSetting update repo's avatar
+func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
+ ctxRepo := ctx.Repo.Repository
+
+ if form.Avatar == nil {
+ // No avatar is uploaded and we not removing it here.
+ // No random avatar generated here.
+ // Just exit, no action.
+ if ctxRepo.CustomAvatarRelativePath() == "" {
+ log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID)
+ }
+ return nil
+ }
+
+ r, err := form.Avatar.Open()
+ if err != nil {
+ return fmt.Errorf("Avatar.Open: %v", err)
+ }
+ defer r.Close()
+
+ if form.Avatar.Size > setting.Avatar.MaxFileSize {
+ return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big"))
+ }
+
+ data, err := ioutil.ReadAll(r)
+ 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 = ctxRepo.UploadAvatar(data); err != nil {
+ return fmt.Errorf("UploadAvatar: %v", err)
+ }
+ return nil
+}
+
+// SettingsAvatar save new POSTed repository avatar
+func SettingsAvatar(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AvatarForm)
+ form.Source = forms.AvatarLocal
+ if err := UpdateAvatarSetting(ctx, *form); err != nil {
+ ctx.Flash.Error(err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success"))
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
+
+// SettingsDeleteAvatar delete repository avatar
+func SettingsDeleteAvatar(ctx *context.Context) {
+ if err := ctx.Repo.Repository.DeleteAvatar(); err != nil {
+ ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
--- /dev/null
+// 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))
+ }
+}
--- /dev/null
+// 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))
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// 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 (
+ "errors"
+ "fmt"
+ "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/convert"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/webhook"
+ jsoniter "github.com/json-iterator/go"
+)
+
+const (
+ tplHooks base.TplName = "repo/settings/webhook/base"
+ tplHookNew base.TplName = "repo/settings/webhook/new"
+ tplOrgHookNew base.TplName = "org/settings/hook_new"
+ tplAdminHookNew base.TplName = "admin/hook_new"
+)
+
+// Webhooks render web hooks list page
+func Webhooks(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.hooks")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
+ ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
+ ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/")
+
+ ws, err := models.GetWebhooksByRepoID(ctx.Repo.Repository.ID, models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetWebhooksByRepoID", err)
+ return
+ }
+ ctx.Data["Webhooks"] = ws
+
+ ctx.HTML(http.StatusOK, tplHooks)
+}
+
+type orgRepoCtx struct {
+ OrgID int64
+ RepoID int64
+ IsAdmin bool
+ IsSystemWebhook bool
+ Link string
+ LinkNew string
+ NewTemplate base.TplName
+}
+
+// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context.
+func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
+ if len(ctx.Repo.RepoLink) > 0 {
+ return &orgRepoCtx{
+ RepoID: ctx.Repo.Repository.ID,
+ Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
+ LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"),
+ NewTemplate: tplHookNew,
+ }, nil
+ }
+
+ if len(ctx.Org.OrgLink) > 0 {
+ return &orgRepoCtx{
+ OrgID: ctx.Org.Organization.ID,
+ Link: path.Join(ctx.Org.OrgLink, "settings/hooks"),
+ LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"),
+ NewTemplate: tplOrgHookNew,
+ }, nil
+ }
+
+ if ctx.User.IsAdmin {
+ // Are we looking at default webhooks?
+ if ctx.Params(":configType") == "default-hooks" {
+ return &orgRepoCtx{
+ IsAdmin: true,
+ Link: path.Join(setting.AppSubURL, "/admin/hooks"),
+ LinkNew: path.Join(setting.AppSubURL, "/admin/default-hooks"),
+ NewTemplate: tplAdminHookNew,
+ }, nil
+ }
+
+ // Must be system webhooks instead
+ return &orgRepoCtx{
+ IsAdmin: true,
+ IsSystemWebhook: true,
+ Link: path.Join(setting.AppSubURL, "/admin/hooks"),
+ LinkNew: path.Join(setting.AppSubURL, "/admin/system-hooks"),
+ NewTemplate: tplAdminHookNew,
+ }, nil
+ }
+
+ return nil, errors.New("Unable to set OrgRepo context")
+}
+
+func checkHookType(ctx *context.Context) string {
+ hookType := strings.ToLower(ctx.Params(":type"))
+ if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) {
+ ctx.NotFound("checkHookType", nil)
+ return ""
+ }
+ return hookType
+}
+
+// WebhooksNew render creating webhook page
+func WebhooksNew(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if orCtx.IsAdmin && orCtx.IsSystemWebhook {
+ ctx.Data["PageIsAdminSystemHooks"] = true
+ ctx.Data["PageIsAdminSystemHooksNew"] = true
+ } else if orCtx.IsAdmin {
+ ctx.Data["PageIsAdminDefaultHooks"] = true
+ ctx.Data["PageIsAdminDefaultHooksNew"] = true
+ } else {
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ }
+
+ hookType := checkHookType(ctx)
+ ctx.Data["HookType"] = hookType
+ if ctx.Written() {
+ return
+ }
+ if hookType == "discord" {
+ ctx.Data["DiscordHook"] = map[string]interface{}{
+ "Username": "Gitea",
+ "IconURL": setting.AppURL + "img/favicon.png",
+ }
+ }
+ ctx.Data["BaseLink"] = orCtx.LinkNew
+
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+}
+
+// ParseHookEvent convert web form content to models.HookEvent
+func ParseHookEvent(form forms.WebhookForm) *models.HookEvent {
+ return &models.HookEvent{
+ PushOnly: form.PushOnly(),
+ SendEverything: form.SendEverything(),
+ ChooseEvents: form.ChooseEvents(),
+ HookEvents: models.HookEvents{
+ Create: form.Create,
+ Delete: form.Delete,
+ Fork: form.Fork,
+ Issues: form.Issues,
+ IssueAssign: form.IssueAssign,
+ IssueLabel: form.IssueLabel,
+ IssueMilestone: form.IssueMilestone,
+ IssueComment: form.IssueComment,
+ Release: form.Release,
+ Push: form.Push,
+ PullRequest: form.PullRequest,
+ PullRequestAssign: form.PullRequestAssign,
+ PullRequestLabel: form.PullRequestLabel,
+ PullRequestMilestone: form.PullRequestMilestone,
+ PullRequestComment: form.PullRequestComment,
+ PullRequestReview: form.PullRequestReview,
+ PullRequestSync: form.PullRequestSync,
+ Repository: form.Repository,
+ },
+ BranchFilter: form.BranchFilter,
+ }
+}
+
+// GiteaHooksNewPost response for creating Gitea webhook
+func GiteaHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewWebhookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.GITEA
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+ ctx.Data["BaseLink"] = orCtx.LinkNew
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ contentType := models.ContentTypeJSON
+ if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+ contentType = models.ContentTypeForm
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ HTTPMethod: form.HTTPMethod,
+ ContentType: contentType,
+ Secret: form.Secret,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.GITEA,
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// GogsHooksNewPost response for creating webhook
+func GogsHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewGogshookForm)
+ newGogsWebhookPost(ctx, *form, models.GOGS)
+}
+
+// newGogsWebhookPost response for creating gogs hook
+func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind models.HookTaskType) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.GOGS
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+ ctx.Data["BaseLink"] = orCtx.LinkNew
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ contentType := models.ContentTypeJSON
+ if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+ contentType = models.ContentTypeForm
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: contentType,
+ Secret: form.Secret,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: kind,
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// DiscordHooksNewPost response for creating discord hook
+func DiscordHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.DISCORD
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.DiscordMeta{
+ Username: form.Username,
+ IconURL: form.IconURL,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.DISCORD,
+ Meta: string(meta),
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// DingtalkHooksNewPost response for creating dingtalk hook
+func DingtalkHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.DINGTALK
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.DINGTALK,
+ Meta: "",
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// TelegramHooksNewPost response for creating telegram hook
+func TelegramHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.TELEGRAM
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.TelegramMeta{
+ BotToken: form.BotToken,
+ ChatID: form.ChatID,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID),
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.TELEGRAM,
+ Meta: string(meta),
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// MatrixHooksNewPost response for creating a Matrix hook
+func MatrixHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.MATRIX
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.MatrixMeta{
+ HomeserverURL: form.HomeserverURL,
+ Room: form.RoomID,
+ AccessToken: form.AccessToken,
+ MessageType: form.MessageType,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID),
+ ContentType: models.ContentTypeJSON,
+ HTTPMethod: "PUT",
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.MATRIX,
+ Meta: string(meta),
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// MSTeamsHooksNewPost response for creating MS Teams hook
+func MSTeamsHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.MSTEAMS
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.MSTEAMS,
+ Meta: "",
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// SlackHooksNewPost response for creating slack hook
+func SlackHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewSlackHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.SLACK
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ if form.HasInvalidChannel() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
+ ctx.Redirect(orCtx.LinkNew + "/slack/new")
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.SlackMeta{
+ Channel: strings.TrimSpace(form.Channel),
+ Username: form.Username,
+ IconURL: form.IconURL,
+ Color: form.Color,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.SLACK,
+ Meta: string(meta),
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+// FeishuHooksNewPost response for creating feishu hook
+func FeishuHooksNewPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksNew"] = true
+ ctx.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}}
+ ctx.Data["HookType"] = models.FEISHU
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w := &models.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: form.PayloadURL,
+ ContentType: models.ContentTypeJSON,
+ HookEvent: ParseHookEvent(form.WebhookForm),
+ IsActive: form.Active,
+ Type: models.FEISHU,
+ Meta: "",
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.CreateWebhook(w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
+}
+
+func checkWebhook(ctx *context.Context) (*orgRepoCtx, *models.Webhook) {
+ ctx.Data["RequireHighlightJS"] = true
+
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return nil, nil
+ }
+ ctx.Data["BaseLink"] = orCtx.Link
+
+ var w *models.Webhook
+ if orCtx.RepoID > 0 {
+ w, err = models.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+ } else if orCtx.OrgID > 0 {
+ w, err = models.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+ } else if orCtx.IsAdmin {
+ w, err = models.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
+ }
+ if err != nil || w == nil {
+ if models.IsErrWebhookNotExist(err) {
+ ctx.NotFound("GetWebhookByID", nil)
+ } else {
+ ctx.ServerError("GetWebhookByID", err)
+ }
+ return nil, nil
+ }
+
+ ctx.Data["HookType"] = w.Type
+ switch w.Type {
+ case models.SLACK:
+ ctx.Data["SlackHook"] = webhook.GetSlackHook(w)
+ case models.DISCORD:
+ ctx.Data["DiscordHook"] = webhook.GetDiscordHook(w)
+ case models.TELEGRAM:
+ ctx.Data["TelegramHook"] = webhook.GetTelegramHook(w)
+ case models.MATRIX:
+ ctx.Data["MatrixHook"] = webhook.GetMatrixHook(w)
+ }
+
+ ctx.Data["History"], err = w.History(1)
+ if err != nil {
+ ctx.ServerError("History", err)
+ }
+ return orCtx, w
+}
+
+// WebHooksEdit render editing web hook page
+func WebHooksEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+}
+
+// WebHooksEditPost response for editing web hook
+func WebHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewWebhookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ contentType := models.ContentTypeJSON
+ if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+ contentType = models.ContentTypeForm
+ }
+
+ w.URL = form.PayloadURL
+ w.ContentType = contentType
+ w.Secret = form.Secret
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ w.HTTPMethod = form.HTTPMethod
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("WebHooksEditPost", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// GogsHooksEditPost response for editing gogs hook
+func GogsHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewGogshookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ contentType := models.ContentTypeJSON
+ if models.HookContentType(form.ContentType) == models.ContentTypeForm {
+ contentType = models.ContentTypeForm
+ }
+
+ w.URL = form.PayloadURL
+ w.ContentType = contentType
+ w.Secret = form.Secret
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("GogsHooksEditPost", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// SlackHooksEditPost response for editing slack hook
+func SlackHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewSlackHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ if form.HasInvalidChannel() {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.SlackMeta{
+ Channel: strings.TrimSpace(form.Channel),
+ Username: form.Username,
+ IconURL: form.IconURL,
+ Color: form.Color,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.Meta = string(meta)
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// DiscordHooksEditPost response for editing discord hook
+func DiscordHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.DiscordMeta{
+ Username: form.Username,
+ IconURL: form.IconURL,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.Meta = string(meta)
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// DingtalkHooksEditPost response for editing discord hook
+func DingtalkHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// TelegramHooksEditPost response for editing discord hook
+func TelegramHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.TelegramMeta{
+ BotToken: form.BotToken,
+ ChatID: form.ChatID,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+ w.Meta = string(meta)
+ w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", form.BotToken, form.ChatID)
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// MatrixHooksEditPost response for editing a Matrix hook
+func MatrixHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+ json := jsoniter.ConfigCompatibleWithStandardLibrary
+ meta, err := json.Marshal(&webhook.MatrixMeta{
+ HomeserverURL: form.HomeserverURL,
+ Room: form.RoomID,
+ AccessToken: form.AccessToken,
+ MessageType: form.MessageType,
+ })
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+ w.Meta = string(meta)
+ w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, form.RoomID)
+
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// MSTeamsHooksEditPost response for editing MS Teams hook
+func MSTeamsHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// FeishuHooksEditPost response for editing feishu hook
+func FeishuHooksEditPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.Data["Webhook"] = w
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
+ return
+ }
+
+ w.URL = form.PayloadURL
+ w.HookEvent = ParseHookEvent(form.WebhookForm)
+ w.IsActive = form.Active
+ if err := w.UpdateEvent(); err != nil {
+ ctx.ServerError("UpdateEvent", err)
+ return
+ } else if err := models.UpdateWebhook(w); err != nil {
+ ctx.ServerError("UpdateWebhook", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
+ ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+}
+
+// TestWebhook test if web hook is work fine
+func TestWebhook(ctx *context.Context) {
+ hookID := ctx.ParamsInt64(":id")
+ w, err := models.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID)
+ if err != nil {
+ ctx.Flash.Error("GetWebhookByID: " + err.Error())
+ ctx.Status(500)
+ return
+ }
+
+ // Grab latest commit or fake one if it's empty repository.
+ commit := ctx.Repo.Commit
+ if commit == nil {
+ ghost := models.NewGhostUser()
+ commit = &git.Commit{
+ ID: git.MustIDFromString(git.EmptySHA),
+ Author: ghost.NewGitSig(),
+ Committer: ghost.NewGitSig(),
+ CommitMessage: "This is a fake commit",
+ }
+ }
+
+ apiUser := convert.ToUserWithAccessMode(ctx.User, models.AccessModeNone)
+ p := &api.PushPayload{
+ Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
+ Before: commit.ID.String(),
+ After: commit.ID.String(),
+ Commits: []*api.PayloadCommit{
+ {
+ ID: commit.ID.String(),
+ Message: commit.Message(),
+ URL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
+ Author: &api.PayloadUser{
+ Name: commit.Author.Name,
+ Email: commit.Author.Email,
+ },
+ Committer: &api.PayloadUser{
+ Name: commit.Committer.Name,
+ Email: commit.Committer.Email,
+ },
+ },
+ },
+ Repo: convert.ToRepo(ctx.Repo.Repository, models.AccessModeNone),
+ Pusher: apiUser,
+ Sender: apiUser,
+ }
+ if err := webhook.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil {
+ ctx.Flash.Error("PrepareWebhook: " + err.Error())
+ ctx.Status(500)
+ } else {
+ ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success"))
+ ctx.Status(200)
+ }
+}
+
+// DeleteWebhook delete a webhook
+func DeleteWebhook(ctx *context.Context) {
+ if err := models.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/hooks",
+ })
+}
--- /dev/null
+// 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/",
+ })
+}
--- /dev/null
+// 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"))
+ }
+}
--- /dev/null
+// 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)
+ }
+}
--- /dev/null
+// 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 user
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth/oauth2"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/eventsource"
+ "code.gitea.io/gitea/modules/hcaptcha"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/password"
+ "code.gitea.io/gitea/modules/recaptcha"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/externalaccount"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/mailer"
+
+ "github.com/markbates/goth"
+ "github.com/tstranex/u2f"
+)
+
+const (
+ // tplMustChangePassword template for updating a user's password
+ tplMustChangePassword = "user/auth/change_passwd"
+ // tplSignIn template for sign in page
+ tplSignIn base.TplName = "user/auth/signin"
+ // tplSignUp template path for sign up page
+ tplSignUp base.TplName = "user/auth/signup"
+ // TplActivate template path for activate user
+ TplActivate base.TplName = "user/auth/activate"
+ tplForgotPassword base.TplName = "user/auth/forgot_passwd"
+ tplResetPassword base.TplName = "user/auth/reset_passwd"
+ tplTwofa base.TplName = "user/auth/twofa"
+ tplTwofaScratch base.TplName = "user/auth/twofa_scratch"
+ tplLinkAccount base.TplName = "user/auth/link_account"
+ tplU2F base.TplName = "user/auth/u2f"
+)
+
+// AutoSignIn reads cookie and try to auto-login.
+func AutoSignIn(ctx *context.Context) (bool, error) {
+ if !models.HasEngine {
+ return false, nil
+ }
+
+ uname := ctx.GetCookie(setting.CookieUserName)
+ if len(uname) == 0 {
+ return false, nil
+ }
+
+ isSucceed := false
+ defer func() {
+ if !isSucceed {
+ log.Trace("auto-login cookie cleared: %s", uname)
+ ctx.DeleteCookie(setting.CookieUserName)
+ ctx.DeleteCookie(setting.CookieRememberName)
+ }
+ }()
+
+ u, err := models.GetUserByName(uname)
+ if err != nil {
+ if !models.IsErrUserNotExist(err) {
+ return false, fmt.Errorf("GetUserByName: %v", err)
+ }
+ return false, nil
+ }
+
+ if val, ok := ctx.GetSuperSecureCookie(
+ base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); !ok || val != u.Name {
+ return false, nil
+ }
+
+ isSucceed = true
+
+ // Set session IDs
+ if err := ctx.Session.Set("uid", u.ID); err != nil {
+ return false, err
+ }
+ if err := ctx.Session.Set("uname", u.Name); err != nil {
+ return false, err
+ }
+ if err := ctx.Session.Release(); err != nil {
+ return false, err
+ }
+
+ middleware.DeleteCSRFCookie(ctx.Resp)
+ return true, nil
+}
+
+func checkAutoLogin(ctx *context.Context) bool {
+ // Check auto-login.
+ isSucceed, err := AutoSignIn(ctx)
+ if err != nil {
+ ctx.ServerError("AutoSignIn", err)
+ return true
+ }
+
+ 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, setting.AppSubURL+string(setting.LandingPageURL))
+ return true
+ }
+
+ return false
+}
+
+// SignIn render sign in page
+func SignIn(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("sign_in")
+
+ // Check auto-login.
+ if checkAutoLogin(ctx) {
+ return
+ }
+
+ orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
+ ctx.Data["OAuth2Providers"] = oauth2Providers
+ ctx.Data["Title"] = ctx.Tr("sign_in")
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
+ ctx.Data["PageIsSignIn"] = true
+ ctx.Data["PageIsLogin"] = true
+ ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
+
+ ctx.HTML(http.StatusOK, tplSignIn)
+}
+
+// SignInPost response for sign in request
+func SignInPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("sign_in")
+
+ orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
+ ctx.Data["OAuth2Providers"] = oauth2Providers
+ ctx.Data["Title"] = ctx.Tr("sign_in")
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
+ ctx.Data["PageIsSignIn"] = true
+ ctx.Data["PageIsLogin"] = true
+ ctx.Data["EnableSSPI"] = models.IsSSPIEnabled()
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplSignIn)
+ return
+ }
+
+ form := web.GetForm(ctx).(*forms.SignInForm)
+ u, err := models.UserSignIn(form.UserName, form.Password)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
+ log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+ } else if models.IsErrEmailAlreadyUsed(err) {
+ ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
+ log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+ } else if models.IsErrUserProhibitLogin(err) {
+ log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+ ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+ ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+ } else if models.IsErrUserInactive(err) {
+ if setting.Service.RegisterEmailConfirm {
+ ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
+ ctx.HTML(http.StatusOK, TplActivate)
+ } else {
+ log.Info("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
+ ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+ ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+ }
+ } else {
+ ctx.ServerError("UserSignIn", err)
+ }
+ return
+ }
+ // If this user is enrolled in 2FA, we can't sign the user in just yet.
+ // Instead, redirect them to the 2FA authentication page.
+ _, err = models.GetTwoFactorByUID(u.ID)
+ if err != nil {
+ if models.IsErrTwoFactorNotEnrolled(err) {
+ handleSignIn(ctx, u, form.Remember)
+ } else {
+ ctx.ServerError("UserSignIn", err)
+ }
+ return
+ }
+
+ // User needs to use 2FA, save data and redirect to 2FA page.
+ if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+ ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
+ return
+ }
+ if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
+ ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
+ return
+ }
+ if err := ctx.Session.Release(); err != nil {
+ ctx.ServerError("UserSignIn: Unable to save session", err)
+ return
+ }
+
+ regs, err := models.GetU2FRegistrationsByUID(u.ID)
+ if err == nil && len(regs) > 0 {
+ ctx.Redirect(setting.AppSubURL + "/user/u2f")
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// TwoFactor shows the user a two-factor authentication page.
+func TwoFactor(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("twofa")
+
+ // Check auto-login.
+ if checkAutoLogin(ctx) {
+ return
+ }
+
+ // Ensure user is in a 2FA session.
+ if ctx.Session.Get("twofaUid") == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplTwofa)
+}
+
+// TwoFactorPost validates a user's two-factor authentication token.
+func TwoFactorPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
+ ctx.Data["Title"] = ctx.Tr("twofa")
+
+ // Ensure user is in a 2FA session.
+ idSess := ctx.Session.Get("twofaUid")
+ if idSess == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+ return
+ }
+
+ id := idSess.(int64)
+ twofa, err := models.GetTwoFactorByUID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ // Validate the passcode with the stored TOTP secret.
+ ok, err := twofa.ValidateTOTP(form.Passcode)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ if ok && twofa.LastUsedPasscode != form.Passcode {
+ remember := ctx.Session.Get("twofaRemember").(bool)
+ u, err := models.GetUserByID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ if ctx.Session.Get("linkAccount") != nil {
+ gothUser := ctx.Session.Get("linkAccountGothUser")
+ if gothUser == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+ return
+ }
+
+ err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User))
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ }
+
+ twofa.LastUsedPasscode = form.Passcode
+ if err = models.UpdateTwoFactor(twofa); err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ handleSignIn(ctx, u, remember)
+ return
+ }
+
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
+}
+
+// TwoFactorScratch shows the scratch code form for two-factor authentication.
+func TwoFactorScratch(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("twofa_scratch")
+
+ // Check auto-login.
+ if checkAutoLogin(ctx) {
+ return
+ }
+
+ // Ensure user is in a 2FA session.
+ if ctx.Session.Get("twofaUid") == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplTwofaScratch)
+}
+
+// TwoFactorScratchPost validates and invalidates a user's two-factor scratch token.
+func TwoFactorScratchPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.TwoFactorScratchAuthForm)
+ ctx.Data["Title"] = ctx.Tr("twofa_scratch")
+
+ // Ensure user is in a 2FA session.
+ idSess := ctx.Session.Get("twofaUid")
+ if idSess == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in 2FA session"))
+ return
+ }
+
+ id := idSess.(int64)
+ twofa, err := models.GetTwoFactorByUID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ // Validate the passcode with the stored TOTP secret.
+ if twofa.VerifyScratchToken(form.Token) {
+ // Invalidate the scratch token.
+ _, err = twofa.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if err = models.UpdateTwoFactor(twofa); err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ remember := ctx.Session.Get("twofaRemember").(bool)
+ u, err := models.GetUserByID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ handleSignInFull(ctx, u, remember, false)
+ ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+ return
+ }
+
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
+}
+
+// U2F shows the U2F login page
+func U2F(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("twofa")
+ ctx.Data["RequireU2F"] = true
+ // Check auto-login.
+ if checkAutoLogin(ctx) {
+ return
+ }
+
+ // Ensure user is in a 2FA session.
+ if ctx.Session.Get("twofaUid") == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplU2F)
+}
+
+// U2FChallenge submits a sign challenge to the browser
+func U2FChallenge(ctx *context.Context) {
+ // Ensure user is in a U2F session.
+ idSess := ctx.Session.Get("twofaUid")
+ if idSess == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+ return
+ }
+ id := idSess.(int64)
+ regs, err := models.GetU2FRegistrationsByUID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if len(regs) == 0 {
+ ctx.ServerError("UserSignIn", errors.New("no device registered"))
+ return
+ }
+ challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
+ if err != nil {
+ ctx.ServerError("u2f.NewChallenge", err)
+ return
+ }
+ if err := ctx.Session.Set("u2fChallenge", challenge); err != nil {
+ ctx.ServerError("UserSignIn: unable to set u2fChallenge in session", err)
+ return
+ }
+ if err := ctx.Session.Release(); err != nil {
+ ctx.ServerError("UserSignIn: unable to store session", err)
+ }
+
+ ctx.JSON(http.StatusOK, challenge.SignRequest(regs.ToRegistrations()))
+}
+
+// U2FSign authenticates the user by signResp
+func U2FSign(ctx *context.Context) {
+ signResp := web.GetForm(ctx).(*u2f.SignResponse)
+ challSess := ctx.Session.Get("u2fChallenge")
+ idSess := ctx.Session.Get("twofaUid")
+ if challSess == nil || idSess == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+ return
+ }
+ challenge := challSess.(*u2f.Challenge)
+ id := idSess.(int64)
+ regs, err := models.GetU2FRegistrationsByUID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ for _, reg := range regs {
+ r, err := reg.Parse()
+ if err != nil {
+ log.Fatal("parsing u2f registration: %v", err)
+ continue
+ }
+ newCounter, authErr := r.Authenticate(*signResp, *challenge, reg.Counter)
+ if authErr == nil {
+ reg.Counter = newCounter
+ user, err := models.GetUserByID(id)
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ remember := ctx.Session.Get("twofaRemember").(bool)
+ if err := reg.UpdateCounter(); err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ if ctx.Session.Get("linkAccount") != nil {
+ gothUser := ctx.Session.Get("linkAccountGothUser")
+ if gothUser == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+ return
+ }
+
+ err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User))
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ }
+ redirect := handleSignInFull(ctx, user, remember, false)
+ if redirect == "" {
+ redirect = setting.AppSubURL + "/"
+ }
+ ctx.PlainText(200, []byte(redirect))
+ return
+ }
+ }
+ ctx.Error(http.StatusUnauthorized)
+}
+
+// This handles the final part of the sign-in process of the user.
+func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
+ handleSignInFull(ctx, u, remember, true)
+}
+
+func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
+ if remember {
+ days := 86400 * setting.LogInRememberDays
+ ctx.SetCookie(setting.CookieUserName, u.Name, days)
+ ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
+ setting.CookieRememberName, u.Name, days)
+ }
+
+ _ = ctx.Session.Delete("openid_verified_uri")
+ _ = ctx.Session.Delete("openid_signin_remember")
+ _ = ctx.Session.Delete("openid_determined_email")
+ _ = ctx.Session.Delete("openid_determined_username")
+ _ = ctx.Session.Delete("twofaUid")
+ _ = ctx.Session.Delete("twofaRemember")
+ _ = ctx.Session.Delete("u2fChallenge")
+ _ = ctx.Session.Delete("linkAccount")
+ if err := ctx.Session.Set("uid", u.ID); err != nil {
+ log.Error("Error setting uid %d in session: %v", u.ID, err)
+ }
+ if err := ctx.Session.Set("uname", u.Name); err != nil {
+ log.Error("Error setting uname %s session: %v", u.Name, err)
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Unable to store session: %v", err)
+ }
+
+ // Language setting of the user overwrites the one previously set
+ // If the user does not have a locale set, we save the current one.
+ if len(u.Language) == 0 {
+ u.Language = ctx.Locale.Language()
+ if err := models.UpdateUserCols(u, "language"); err != nil {
+ log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
+ return setting.AppSubURL + "/"
+ }
+ }
+
+ middleware.SetLocaleCookie(ctx.Resp, u.Language, 0)
+
+ // Clear whatever CSRF has right now, force to generate a new one
+ middleware.DeleteCSRFCookie(ctx.Resp)
+
+ // Register last login
+ u.SetLastLogin()
+ if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
+ ctx.ServerError("UpdateUserCols", err)
+ return setting.AppSubURL + "/"
+ }
+
+ if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ if obeyRedirect {
+ ctx.RedirectToFirst(redirectTo)
+ }
+ return redirectTo
+ }
+
+ if obeyRedirect {
+ ctx.Redirect(setting.AppSubURL + "/")
+ }
+ return setting.AppSubURL + "/"
+}
+
+// SignInOAuth handles the OAuth2 login buttons
+func SignInOAuth(ctx *context.Context) {
+ provider := ctx.Params(":provider")
+
+ loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
+ if err != nil {
+ ctx.ServerError("SignIn", err)
+ return
+ }
+
+ // try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
+ user, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
+ if err == nil && user != nil {
+ // we got the user without going through the whole OAuth2 authentication flow again
+ handleOAuth2SignIn(ctx, user, gothUser)
+ return
+ }
+
+ if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+ if strings.Contains(err.Error(), "no provider for ") {
+ if err = models.ResetOAuth2(); err != nil {
+ ctx.ServerError("SignIn", err)
+ return
+ }
+ if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+ ctx.ServerError("SignIn", err)
+ }
+ return
+ }
+ ctx.ServerError("SignIn", err)
+ }
+ // redirect is done in oauth2.Auth
+}
+
+// SignInOAuthCallback handles the callback from the given provider
+func SignInOAuthCallback(ctx *context.Context) {
+ provider := ctx.Params(":provider")
+
+ // first look if the provider is still active
+ loginSource, err := models.GetActiveOAuth2LoginSourceByName(provider)
+ if err != nil {
+ ctx.ServerError("SignIn", err)
+ return
+ }
+
+ if loginSource == nil {
+ ctx.ServerError("SignIn", errors.New("No valid provider found, check configured callback url in provider"))
+ return
+ }
+
+ u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp)
+
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ if u == nil {
+ if !(setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration) && setting.OAuth2Client.EnableAutoRegistration {
+ // create new user with details from oauth2 provider
+ var missingFields []string
+ if gothUser.UserID == "" {
+ missingFields = append(missingFields, "sub")
+ }
+ if gothUser.Email == "" {
+ missingFields = append(missingFields, "email")
+ }
+ if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
+ missingFields = append(missingFields, "nickname")
+ }
+ if len(missingFields) > 0 {
+ log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
+ if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
+ log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
+ }
+ err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
+ ctx.ServerError("CreateUser", err)
+ return
+ }
+ u = &models.User{
+ Name: getUserName(&gothUser),
+ FullName: gothUser.Name,
+ Email: gothUser.Email,
+ IsActive: !setting.OAuth2Client.RegisterEmailConfirm,
+ LoginType: models.LoginOAuth2,
+ LoginSource: loginSource.ID,
+ LoginName: gothUser.UserID,
+ }
+
+ if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
+ // error already handled
+ return
+ }
+ } else {
+ // no existing user is found, request attach or new account
+ showLinkingLogin(ctx, gothUser)
+ return
+ }
+ }
+
+ handleOAuth2SignIn(ctx, u, gothUser)
+}
+
+func getUserName(gothUser *goth.User) string {
+ switch setting.OAuth2Client.Username {
+ case setting.OAuth2UsernameEmail:
+ return strings.Split(gothUser.Email, "@")[0]
+ case setting.OAuth2UsernameNickname:
+ return gothUser.NickName
+ default: // OAuth2UsernameUserid
+ return gothUser.UserID
+ }
+}
+
+func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
+ if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
+ log.Error("Error setting linkAccountGothUser in session: %v", err)
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Error storing session: %v", err)
+ }
+ ctx.Redirect(setting.AppSubURL + "/user/link_account")
+}
+
+func updateAvatarIfNeed(url string, u *models.User) {
+ if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
+ resp, err := http.Get(url)
+ if err == nil {
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+ }
+ // ignore any error
+ if err == nil && resp.StatusCode == http.StatusOK {
+ data, err := ioutil.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
+ if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
+ _ = u.UploadAvatar(data)
+ }
+ }
+ }
+}
+
+func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User) {
+ updateAvatarIfNeed(gothUser.AvatarURL, u)
+
+ // If this user is enrolled in 2FA, we can't sign the user in just yet.
+ // Instead, redirect them to the 2FA authentication page.
+ _, err := models.GetTwoFactorByUID(u.ID)
+ if err != nil {
+ if !models.IsErrTwoFactorNotEnrolled(err) {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ if err := ctx.Session.Set("uid", u.ID); err != nil {
+ log.Error("Error setting uid in session: %v", err)
+ }
+ if err := ctx.Session.Set("uname", u.Name); err != nil {
+ log.Error("Error setting uname in session: %v", err)
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Error storing session: %v", err)
+ }
+
+ // Clear whatever CSRF has right now, force to generate a new one
+ middleware.DeleteCSRFCookie(ctx.Resp)
+
+ // Register last login
+ u.SetLastLogin()
+ if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
+ ctx.ServerError("UpdateUserCols", err)
+ return
+ }
+
+ // update external user information
+ if err := models.UpdateExternalUser(u, gothUser); err != nil {
+ log.Error("UpdateExternalUser failed: %v", err)
+ }
+
+ if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 {
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ ctx.RedirectToFirst(redirectTo)
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/")
+ return
+ }
+
+ // User needs to use 2FA, save data and redirect to 2FA page.
+ if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+ log.Error("Error setting twofaUid in session: %v", err)
+ }
+ if err := ctx.Session.Set("twofaRemember", false); err != nil {
+ log.Error("Error setting twofaRemember in session: %v", err)
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Error storing session: %v", err)
+ }
+
+ // If U2F is enrolled -> Redirect to U2F instead
+ regs, err := models.GetU2FRegistrationsByUID(u.ID)
+ if err == nil && len(regs) > 0 {
+ ctx.Redirect(setting.AppSubURL + "/user/u2f")
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
+// login the user
+func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
+ gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
+
+ if err != nil {
+ if err.Error() == "securecookie: the value is too long" {
+ log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
+ err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
+ }
+ return nil, goth.User{}, err
+ }
+
+ user := &models.User{
+ LoginName: gothUser.UserID,
+ LoginType: models.LoginOAuth2,
+ LoginSource: loginSource.ID,
+ }
+
+ hasUser, err := models.GetUser(user)
+ if err != nil {
+ return nil, goth.User{}, err
+ }
+
+ if hasUser {
+ return user, gothUser, nil
+ }
+
+ // search in external linked users
+ externalLoginUser := &models.ExternalLoginUser{
+ ExternalID: gothUser.UserID,
+ LoginSourceID: loginSource.ID,
+ }
+ hasUser, err = models.GetExternalLogin(externalLoginUser)
+ if err != nil {
+ return nil, goth.User{}, err
+ }
+ if hasUser {
+ user, err = models.GetUserByID(externalLoginUser.UserID)
+ return user, gothUser, err
+ }
+
+ // no user found to login
+ return nil, gothUser, nil
+
+}
+
+// LinkAccount shows the page where the user can decide to login or create a new account
+func LinkAccount(ctx *context.Context) {
+ ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+ ctx.Data["Title"] = ctx.Tr("link_account")
+ ctx.Data["LinkAccountMode"] = true
+ ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+ ctx.Data["Captcha"] = context.GetImageCaptcha()
+ ctx.Data["CaptchaType"] = setting.Service.CaptchaType
+ ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+ ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
+ ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+ ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+ ctx.Data["ShowRegistrationButton"] = false
+
+ // use this to set the right link into the signIn and signUp templates in the link_account template
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+ ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+ gothUser := ctx.Session.Get("linkAccountGothUser")
+ if gothUser == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+ return
+ }
+
+ gu, _ := gothUser.(goth.User)
+ uname := getUserName(&gu)
+ email := gu.Email
+ ctx.Data["user_name"] = uname
+ ctx.Data["email"] = email
+
+ if len(email) != 0 {
+ u, err := models.GetUserByEmail(email)
+ if err != nil && !models.IsErrUserNotExist(err) {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if u != nil {
+ ctx.Data["user_exists"] = true
+ }
+ } else if len(uname) != 0 {
+ u, err := models.GetUserByName(uname)
+ if err != nil && !models.IsErrUserNotExist(err) {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if u != nil {
+ ctx.Data["user_exists"] = true
+ }
+ }
+
+ ctx.HTML(http.StatusOK, tplLinkAccount)
+}
+
+// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
+func LinkAccountPostSignIn(ctx *context.Context) {
+ signInForm := web.GetForm(ctx).(*forms.SignInForm)
+ ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+ ctx.Data["Title"] = ctx.Tr("link_account")
+ ctx.Data["LinkAccountMode"] = true
+ ctx.Data["LinkAccountModeSignIn"] = true
+ ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+ 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["DisableRegistration"] = setting.Service.DisableRegistration
+ ctx.Data["ShowRegistrationButton"] = false
+
+ // use this to set the right link into the signIn and signUp templates in the link_account template
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+ ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+ gothUser := ctx.Session.Get("linkAccountGothUser")
+ if gothUser == nil {
+ ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplLinkAccount)
+ return
+ }
+
+ u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.Data["user_exists"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplLinkAccount, &signInForm)
+ } else {
+ ctx.ServerError("UserLinkAccount", err)
+ }
+ return
+ }
+
+ linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
+}
+
+func linkAccount(ctx *context.Context, u *models.User, gothUser goth.User, remember bool) {
+ updateAvatarIfNeed(gothUser.AvatarURL, u)
+
+ // If this user is enrolled in 2FA, we can't sign the user in just yet.
+ // Instead, redirect them to the 2FA authentication page.
+ _, err := models.GetTwoFactorByUID(u.ID)
+ if err != nil {
+ if !models.IsErrTwoFactorNotEnrolled(err) {
+ ctx.ServerError("UserLinkAccount", err)
+ return
+ }
+
+ err = externalaccount.LinkAccountToUser(u, gothUser)
+ if err != nil {
+ ctx.ServerError("UserLinkAccount", err)
+ return
+ }
+
+ handleSignIn(ctx, u, remember)
+ return
+ }
+
+ // User needs to use 2FA, save data and redirect to 2FA page.
+ if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
+ log.Error("Error setting twofaUid in session: %v", err)
+ }
+ if err := ctx.Session.Set("twofaRemember", remember); err != nil {
+ log.Error("Error setting twofaRemember in session: %v", err)
+ }
+ if err := ctx.Session.Set("linkAccount", true); err != nil {
+ log.Error("Error setting linkAccount in session: %v", err)
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Error storing session: %v", err)
+ }
+
+ // If U2F is enrolled -> Redirect to U2F instead
+ regs, err := models.GetU2FRegistrationsByUID(u.ID)
+ if err == nil && len(regs) > 0 {
+ ctx.Redirect(setting.AppSubURL + "/user/u2f")
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/two_factor")
+}
+
+// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
+func LinkAccountPostRegister(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RegisterForm)
+ // TODO Make insecure passwords optional for local accounts also,
+ // once email-based Second-Factor Auth is available
+ ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
+ ctx.Data["Title"] = ctx.Tr("link_account")
+ ctx.Data["LinkAccountMode"] = true
+ ctx.Data["LinkAccountModeRegister"] = true
+ ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
+ 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["DisableRegistration"] = setting.Service.DisableRegistration
+ ctx.Data["ShowRegistrationButton"] = false
+
+ // use this to set the right link into the signIn and signUp templates in the link_account template
+ ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
+ ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
+
+ gothUserInterface := ctx.Session.Get("linkAccountGothUser")
+ if gothUserInterface == nil {
+ ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
+ return
+ }
+ gothUser, ok := gothUserInterface.(goth.User)
+ if !ok {
+ ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplLinkAccount)
+ return
+ }
+
+ if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
+ var valid bool
+ var err error
+ switch setting.Service.CaptchaType {
+ case setting.ImageCaptcha:
+ valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
+ case setting.ReCaptcha:
+ valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
+ case setting.HCaptcha:
+ 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"), tplLinkAccount, &form)
+ return
+ }
+ }
+
+ if !form.IsEmailDomainAllowed() {
+ ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
+ return
+ }
+
+ if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
+ // In models.User an empty password is classed as not set, so we set form.Password to empty.
+ // Eventually the database should be changed to indicate "Second Factor"-enabled accounts
+ // (accounts that do not introduce the security vulnerabilities of a password).
+ // If a user decides to circumvent second-factor security, and purposefully create a password,
+ // they can still do so using the "Recover Account" option.
+ form.Password = ""
+ } else {
+ if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
+ return
+ }
+ if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
+ return
+ }
+ }
+
+ loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider)
+ if err != nil {
+ ctx.ServerError("CreateUser", err)
+ }
+
+ u := &models.User{
+ Name: form.UserName,
+ Email: form.Email,
+ Passwd: form.Password,
+ IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+ LoginType: models.LoginOAuth2,
+ LoginSource: loginSource.ID,
+ LoginName: gothUser.UserID,
+ }
+
+ if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, &gothUser, false) {
+ // error already handled
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+}
+
+// HandleSignOut resets the session and sets the cookies
+func HandleSignOut(ctx *context.Context) {
+ _ = ctx.Session.Flush()
+ _ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
+ ctx.DeleteCookie(setting.CookieUserName)
+ ctx.DeleteCookie(setting.CookieRememberName)
+ middleware.DeleteCSRFCookie(ctx.Resp)
+ middleware.DeleteLocaleCookie(ctx.Resp)
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+}
+
+// SignOut sign out from login status
+func SignOut(ctx *context.Context) {
+ if ctx.User != nil {
+ eventsource.GetManager().SendMessageBlocking(ctx.User.ID, &eventsource.Event{
+ Name: "logout",
+ Data: ctx.Session.ID(),
+ })
+ }
+ HandleSignOut(ctx)
+ ctx.Redirect(setting.AppSubURL + "/")
+}
+
+// SignUp render the register page
+func SignUp(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("sign_up")
+
+ ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+
+ 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["PageIsSignUp"] = true
+
+ //Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
+ ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
+
+ ctx.HTML(http.StatusOK, tplSignUp)
+}
+
+// SignUpPost response for sign up information submission
+func SignUpPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RegisterForm)
+ ctx.Data["Title"] = ctx.Tr("sign_up")
+
+ ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+
+ 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["PageIsSignUp"] = true
+
+ //Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
+ if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplSignUp)
+ 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:
+ valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
+ case setting.HCaptcha:
+ 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"), tplSignUp, &form)
+ return
+ }
+ }
+
+ if !form.IsEmailDomainAllowed() {
+ ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
+ return
+ }
+
+ if form.Password != form.Retype {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
+ return
+ }
+ if len(form.Password) < setting.MinPasswordLength {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
+ return
+ }
+ if !password.IsComplexEnough(form.Password) {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form)
+ return
+ }
+ pwned, err := password.IsPwned(ctx, form.Password)
+ if pwned {
+ errMsg := ctx.Tr("auth.password_pwned")
+ if err != nil {
+ log.Error(err.Error())
+ errMsg = ctx.Tr("auth.password_pwned_err")
+ }
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(errMsg, tplSignUp, &form)
+ return
+ }
+
+ u := &models.User{
+ Name: form.UserName,
+ Email: form.Email,
+ Passwd: form.Password,
+ IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+ }
+
+ if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, false) {
+ // error already handled
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("auth.sign_up_successful"))
+ handleSignInFull(ctx, u, false, true)
+}
+
+// createAndHandleCreatedUser calls createUserInContext and
+// then handleUserCreated.
+func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) bool {
+ if !createUserInContext(ctx, tpl, form, u, gothUser, allowLink) {
+ return false
+ }
+ return handleUserCreated(ctx, u, gothUser)
+}
+
+// createUserInContext creates a user and handles errors within a given context.
+// Optionally a template can be specified.
+func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *models.User, gothUser *goth.User, allowLink bool) (ok bool) {
+ if err := models.CreateUser(u); err != nil {
+ if allowLink && (models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err)) {
+ if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
+ var user *models.User
+ user = &models.User{Name: u.Name}
+ hasUser, err := models.GetUser(user)
+ if !hasUser || err != nil {
+ user = &models.User{Email: u.Email}
+ hasUser, err = models.GetUser(user)
+ if !hasUser || err != nil {
+ ctx.ServerError("UserLinkAccount", err)
+ return
+ }
+ }
+
+ // TODO: probably we should respect 'remember' user's choice...
+ linkAccount(ctx, user, *gothUser, true)
+ return // user is already created here, all redirects are handled
+ } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
+ showLinkingLogin(ctx, *gothUser)
+ return // user will be created only after linking login
+ }
+ }
+
+ // handle error without template
+ if len(tpl) == 0 {
+ ctx.ServerError("CreateUser", err)
+ return
+ }
+
+ // handle error with template
+ switch {
+ case models.IsErrUserAlreadyExist(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
+ case models.IsErrEmailAlreadyUsed(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
+ case models.IsErrEmailInvalid(err):
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
+ case models.IsErrNameReserved(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form)
+ case models.IsErrNamePatternNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form)
+ case models.IsErrNameCharsNotAllowed(err):
+ ctx.Data["Err_UserName"] = true
+ ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tpl, form)
+ default:
+ ctx.ServerError("CreateUser", err)
+ }
+ return
+ }
+ log.Trace("Account created: %s", u.Name)
+ return true
+}
+
+// handleUserCreated does additional steps after a new user is created.
+// It auto-sets admin for the only user, updates the optional external user and
+// sends a confirmation email if required.
+func handleUserCreated(ctx *context.Context, u *models.User, gothUser *goth.User) (ok bool) {
+ // Auto-set admin for the only user.
+ if models.CountUsers() == 1 {
+ u.IsAdmin = true
+ u.IsActive = true
+ u.SetLastLogin()
+ if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ }
+
+ // update external user information
+ if gothUser != nil {
+ if err := models.UpdateExternalUser(u, *gothUser); err != nil {
+ log.Error("UpdateExternalUser failed: %v", err)
+ }
+ }
+
+ // Send confirmation email
+ if !u.IsActive && u.ID > 1 {
+ mailer.SendActivateAccountMail(ctx.Locale, u)
+
+ ctx.Data["IsSendRegisterMail"] = true
+ ctx.Data["Email"] = u.Email
+ ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+ ctx.HTML(http.StatusOK, TplActivate)
+
+ if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
+ return
+ }
+
+ return true
+}
+
+// Activate render activate user page
+func Activate(ctx *context.Context) {
+ code := ctx.Query("code")
+
+ if len(code) == 0 {
+ ctx.Data["IsActivatePage"] = true
+ if ctx.User == nil || ctx.User.IsActive {
+ ctx.NotFound("invalid user", nil)
+ return
+ }
+ // Resend confirmation email.
+ if setting.Service.RegisterEmailConfirm {
+ if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
+ ctx.Data["ResendLimited"] = true
+ } else {
+ ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+ mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
+
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
+ }
+ } else {
+ ctx.Data["ServiceNotEnabled"] = true
+ }
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+
+ user := models.VerifyUserActiveCode(code)
+ // if code is wrong
+ if user == nil {
+ ctx.Data["IsActivateFailed"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+
+ // if account is local account, verify password
+ if user.LoginSource == 0 {
+ ctx.Data["Code"] = code
+ ctx.Data["NeedsPassword"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+
+ handleAccountActivation(ctx, user)
+}
+
+// ActivatePost handles account activation with password check
+func ActivatePost(ctx *context.Context) {
+ code := ctx.Query("code")
+ if len(code) == 0 {
+ ctx.Redirect(setting.AppSubURL + "/user/activate")
+ return
+ }
+
+ user := models.VerifyUserActiveCode(code)
+ // if code is wrong
+ if user == nil {
+ ctx.Data["IsActivateFailed"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+
+ // if account is local account, verify password
+ if user.LoginSource == 0 {
+ password := ctx.Query("password")
+ if len(password) == 0 {
+ ctx.Data["Code"] = code
+ ctx.Data["NeedsPassword"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+ if !user.ValidatePassword(password) {
+ ctx.Data["IsActivateFailed"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+ return
+ }
+ }
+
+ handleAccountActivation(ctx, user)
+}
+
+func handleAccountActivation(ctx *context.Context, user *models.User) {
+ user.IsActive = true
+ var err error
+ if user.Rands, err = models.GetUserSalt(); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.NotFound("UpdateUserCols", err)
+ } else {
+ ctx.ServerError("UpdateUser", err)
+ }
+ return
+ }
+
+ log.Trace("User activated: %s", user.Name)
+
+ if err := ctx.Session.Set("uid", user.ID); err != nil {
+ log.Error(fmt.Sprintf("Error setting uid in session: %v", err))
+ }
+ if err := ctx.Session.Set("uname", user.Name); err != nil {
+ log.Error(fmt.Sprintf("Error setting uname in session: %v", err))
+ }
+ if err := ctx.Session.Release(); err != nil {
+ log.Error("Error storing session: %v", err)
+ }
+
+ ctx.Flash.Success(ctx.Tr("auth.account_activated"))
+ ctx.Redirect(setting.AppSubURL + "/")
+}
+
+// ActivateEmail render the activate email page
+func ActivateEmail(ctx *context.Context) {
+ code := ctx.Query("code")
+ emailStr := ctx.Query("email")
+
+ // Verify code.
+ if email := models.VerifyActiveEmailCode(code, emailStr); email != nil {
+ if err := email.Activate(); err != nil {
+ ctx.ServerError("ActivateEmail", err)
+ }
+
+ log.Trace("Email activated: %s", email.Email)
+ ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+
+ if u, err := models.GetUserByID(email.UID); err != nil {
+ log.Warn("GetUserByID: %d", email.UID)
+ } else {
+ // Allow user to validate more emails
+ _ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
+ }
+ }
+
+ // FIXME: e-mail verification does not require the user to be logged in,
+ // so this could be redirecting to the login page.
+ // Should users be logged in automatically here? (consider 2FA requirements, etc.)
+ ctx.Redirect(setting.AppSubURL + "/user/settings/account")
+}
+
+// ForgotPasswd render the forget pasword page
+func ForgotPasswd(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+ if setting.MailService == nil {
+ ctx.Data["IsResetDisable"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ email := ctx.Query("email")
+ ctx.Data["Email"] = email
+
+ ctx.Data["IsResetRequest"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+// ForgotPasswdPost response for forget password request
+func ForgotPasswdPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
+
+ if setting.MailService == nil {
+ ctx.NotFound("ForgotPasswdPost", nil)
+ return
+ }
+ ctx.Data["IsResetRequest"] = true
+
+ email := ctx.Query("email")
+ ctx.Data["Email"] = email
+
+ u, err := models.GetUserByEmail(email)
+ if err != nil {
+ if models.IsErrUserNotExist(err) {
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+ ctx.Data["IsResetSent"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ ctx.ServerError("user.ResetPasswd(check existence)", err)
+ return
+ }
+
+ if !u.IsLocal() && !u.IsOAuth2() {
+ ctx.Data["Err_Email"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
+ return
+ }
+
+ if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+ ctx.Data["ResendLimited"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+ return
+ }
+
+ mailer.SendResetPasswordMail(u)
+
+ if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
+
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+ ctx.Data["IsResetSent"] = true
+ ctx.HTML(http.StatusOK, tplForgotPassword)
+}
+
+func commonResetPassword(ctx *context.Context) (*models.User, *models.TwoFactor) {
+ code := ctx.Query("code")
+
+ ctx.Data["Title"] = ctx.Tr("auth.reset_password")
+ ctx.Data["Code"] = code
+
+ if nil != ctx.User {
+ ctx.Data["user_signed_in"] = true
+ }
+
+ if len(code) == 0 {
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
+ return nil, nil
+ }
+
+ // Fail early, don't frustrate the user
+ u := models.VerifyUserActiveCode(code)
+ if u == nil {
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code"))
+ return nil, nil
+ }
+
+ twofa, err := models.GetTwoFactorByUID(u.ID)
+ if err != nil {
+ if !models.IsErrTwoFactorNotEnrolled(err) {
+ ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
+ return nil, nil
+ }
+ } else {
+ ctx.Data["has_two_factor"] = true
+ ctx.Data["scratch_code"] = ctx.QueryBool("scratch_code")
+ }
+
+ // Show the user that they are affecting the account that they intended to
+ ctx.Data["user_email"] = u.Email
+
+ if nil != ctx.User && u.ID != ctx.User.ID {
+ ctx.Flash.Error(ctx.Tr("auth.reset_password_wrong_user", ctx.User.Email, u.Email))
+ return nil, nil
+ }
+
+ return u, twofa
+}
+
+// ResetPasswd render the account recovery page
+func ResetPasswd(ctx *context.Context) {
+ ctx.Data["IsResetForm"] = true
+
+ commonResetPassword(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplResetPassword)
+}
+
+// ResetPasswdPost response from account recovery request
+func ResetPasswdPost(ctx *context.Context) {
+ u, twofa := commonResetPassword(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if u == nil {
+ // Flash error has been set
+ ctx.HTML(http.StatusOK, tplResetPassword)
+ return
+ }
+
+ // Validate password length.
+ passwd := ctx.Query("password")
+ if len(passwd) < setting.MinPasswordLength {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
+ return
+ } else if !password.IsComplexEnough(passwd) {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil)
+ return
+ } else if pwned, err := password.IsPwned(ctx, passwd); pwned || err != nil {
+ errMsg := ctx.Tr("auth.password_pwned")
+ if err != nil {
+ log.Error(err.Error())
+ errMsg = ctx.Tr("auth.password_pwned_err")
+ }
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(errMsg, tplResetPassword, nil)
+ return
+ }
+
+ // Handle two-factor
+ regenerateScratchToken := false
+ if twofa != nil {
+ if ctx.QueryBool("scratch_code") {
+ if !twofa.VerifyScratchToken(ctx.Query("token")) {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Token"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
+ return
+ }
+ regenerateScratchToken = true
+ } else {
+ passcode := ctx.Query("passcode")
+ ok, err := twofa.ValidateTOTP(passcode)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
+ return
+ }
+ if !ok || twofa.LastUsedPasscode == passcode {
+ ctx.Data["IsResetForm"] = true
+ ctx.Data["Err_Passcode"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
+ return
+ }
+
+ twofa.LastUsedPasscode = passcode
+ if err = models.UpdateTwoFactor(twofa); err != nil {
+ ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
+ return
+ }
+ }
+ }
+ var err error
+ if u.Rands, err = models.GetUserSalt(); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ if err = u.SetPassword(passwd); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+ u.MustChangePassword = false
+ if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+
+ log.Trace("User password reset: %s", u.Name)
+ ctx.Data["IsResetFailed"] = true
+ remember := len(ctx.Query("remember")) != 0
+
+ if regenerateScratchToken {
+ // Invalidate the scratch token.
+ _, err = twofa.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+ if err = models.UpdateTwoFactor(twofa); err != nil {
+ ctx.ServerError("UserSignIn", err)
+ return
+ }
+
+ handleSignInFull(ctx, u, remember, false)
+ ctx.Flash.Info(ctx.Tr("auth.twofa_scratch_used"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+ return
+ }
+
+ handleSignInFull(ctx, u, remember, true)
+}
+
+// MustChangePassword renders the page to change a user's password
+func MustChangePassword(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+ ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+ ctx.Data["MustChangePassword"] = true
+ ctx.HTML(http.StatusOK, tplMustChangePassword)
+}
+
+// MustChangePasswordPost response for updating a user's password after his/her
+// account was created by an admin
+func MustChangePasswordPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
+ ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+ ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password"
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplMustChangePassword)
+ return
+ }
+ u := ctx.User
+ // Make sure only requests for users who are eligible to change their password via
+ // this method passes through
+ if !u.MustChangePassword {
+ ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page"))
+ return
+ }
+
+ if form.Password != form.Retype {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
+ return
+ }
+
+ if len(form.Password) < setting.MinPasswordLength {
+ ctx.Data["Err_Password"] = true
+ ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
+ return
+ }
+
+ var err error
+ if err = u.SetPassword(form.Password); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+
+ u.MustChangePassword = false
+
+ if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
+
+ log.Trace("User updated password: %s", u.Name)
+
+ if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ ctx.RedirectToFirst(redirectTo)
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/")
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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 = ©OfGravatarSourceURL
+ 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))
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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("..", "..", ".."))
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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))
+ }
+}
--- /dev/null
+// 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())
+}
--- /dev/null
+// 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())
+ }
+}
--- /dev/null
+// 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())
+ }
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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
+ }
+ }
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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("..", "..", "..", ".."))
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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")
+}
--- /dev/null
+// 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",
+ })
+}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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 web
+
+import (
+ "encoding/gob"
+ "net/http"
+ "os"
+ "path"
+
+ "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/metrics"
+ "code.gitea.io/gitea/modules/public"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/validation"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/misc"
+ "code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/routers/web/admin"
+ "code.gitea.io/gitea/routers/web/dev"
+ "code.gitea.io/gitea/routers/web/events"
+ "code.gitea.io/gitea/routers/web/explore"
+ "code.gitea.io/gitea/routers/web/org"
+ "code.gitea.io/gitea/routers/web/repo"
+ "code.gitea.io/gitea/routers/web/user"
+ userSetting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/lfs"
+ "code.gitea.io/gitea/services/mailer"
+
+ // to registers all internal adapters
+ _ "code.gitea.io/gitea/modules/session"
+
+ "gitea.com/go-chi/captcha"
+ "gitea.com/go-chi/session"
+ "github.com/NYTimes/gziphandler"
+ "github.com/go-chi/chi/middleware"
+ "github.com/go-chi/cors"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/tstranex/u2f"
+)
+
+const (
+ // GzipMinSize represents min size to compress for the body size of response
+ GzipMinSize = 1400
+)
+
+// CorsHandler return a http handler who set CORS options if enabled by config
+func CorsHandler() func(next http.Handler) http.Handler {
+ if setting.CORSConfig.Enabled {
+ return cors.Handler(cors.Options{
+ //Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
+ AllowedOrigins: setting.CORSConfig.AllowDomain,
+ //setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
+ AllowedMethods: setting.CORSConfig.Methods,
+ AllowCredentials: setting.CORSConfig.AllowCredentials,
+ MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
+ })
+ }
+
+ return func(next http.Handler) http.Handler {
+ return next
+ }
+}
+
+// Routes returns all web routes
+func Routes() *web.Route {
+ routes := web.NewRoute()
+
+ routes.Use(public.AssetsHandler(&public.Options{
+ Directory: path.Join(setting.StaticRootPath, "public"),
+ Prefix: "/assets",
+ CorsHandler: CorsHandler(),
+ }))
+
+ routes.Use(session.Sessioner(session.Options{
+ Provider: setting.SessionConfig.Provider,
+ ProviderConfig: setting.SessionConfig.ProviderConfig,
+ CookieName: setting.SessionConfig.CookieName,
+ CookiePath: setting.SessionConfig.CookiePath,
+ Gclifetime: setting.SessionConfig.Gclifetime,
+ Maxlifetime: setting.SessionConfig.Maxlifetime,
+ Secure: setting.SessionConfig.Secure,
+ SameSite: setting.SessionConfig.SameSite,
+ Domain: setting.SessionConfig.Domain,
+ }))
+
+ routes.Use(Recovery())
+
+ // We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
+ routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
+ routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
+
+ // for health check - doeesn't need to be passed through gzip handler
+ routes.Head("/", func(w http.ResponseWriter, req *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ // this png is very likely to always be below the limit for gzip so it doesn't need to pass through gzip
+ routes.Get("/apple-touch-icon.png", func(w http.ResponseWriter, req *http.Request) {
+ http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/apple-touch-icon.png"), 301)
+ })
+
+ gob.Register(&u2f.Challenge{})
+
+ common := []interface{}{}
+
+ if setting.EnableGzip {
+ h, err := gziphandler.GzipHandlerWithOpts(gziphandler.MinSize(GzipMinSize))
+ if err != nil {
+ log.Fatal("GzipHandlerWithOpts failed: %v", err)
+ }
+ common = append(common, h)
+ }
+
+ mailer.InitMailRender(templates.Mailer())
+
+ if setting.Service.EnableCaptcha {
+ // The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
+ routes.Route("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...)
+ }
+
+ if setting.HasRobotsTxt {
+ routes.Get("/robots.txt", append(common, func(w http.ResponseWriter, req *http.Request) {
+ filePath := path.Join(setting.CustomPath, "robots.txt")
+ fi, err := os.Stat(filePath)
+ if err == nil && httpcache.HandleTimeCache(req, w, fi) {
+ return
+ }
+ http.ServeFile(w, req, filePath)
+ })...)
+ }
+
+ // prometheus metrics endpoint - do not need to go through contexter
+ if setting.Metrics.Enabled {
+ c := metrics.NewCollector()
+ prometheus.MustRegister(c)
+
+ routes.Get("/metrics", append(common, Metrics)...)
+ }
+
+ // Removed: toolbox.Toolboxer middleware will provide debug informations which seems unnecessary
+ common = append(common, context.Contexter())
+
+ // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
+ common = append(common, middleware.GetHead)
+
+ if setting.API.EnableSwagger {
+ // Note: The route moved from apiroutes because it's in fact want to render a web page
+ routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default
+ }
+
+ // TODO: These really seem like things that could be folded into Contexter or as helper functions
+ common = append(common, user.GetNotificationCount)
+ common = append(common, repo.GetActiveStopwatch)
+ common = append(common, goGet)
+
+ others := web.NewRoute()
+ for _, middle := range common {
+ others.Use(middle)
+ }
+
+ RegisterRoutes(others)
+ routes.Mount("", others)
+ return routes
+}
+
+// RegisterRoutes register routes
+func RegisterRoutes(m *web.Route) {
+ reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
+ ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
+ ignExploreSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
+ ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
+ reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
+
+ //bindIgnErr := binding.BindIgnErr
+ bindIgnErr := web.Bind
+ validation.AddBindingRules()
+
+ openIDSignInEnabled := func(ctx *context.Context) {
+ if !setting.Service.EnableOpenIDSignIn {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ openIDSignUpEnabled := func(ctx *context.Context) {
+ if !setting.Service.EnableOpenIDSignUp {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
+ if !setting.Service.ShowMilestonesDashboardPage {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ // webhooksEnabled requires webhooks to be enabled by admin.
+ webhooksEnabled := func(ctx *context.Context) {
+ if setting.DisableWebhooks {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ lfsServerEnabled := func(ctx *context.Context) {
+ if !setting.LFS.StartServer {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
+ // FIXME: not all routes need go through same middleware.
+ // Especially some AJAX requests, we can reduce middleware number to improve performance.
+ // Routers.
+ // for health check
+ m.Get("/", Home)
+ m.Get("/.well-known/openid-configuration", user.OIDCWellKnown)
+ m.Group("/explore", func() {
+ m.Get("", func(ctx *context.Context) {
+ ctx.Redirect(setting.AppSubURL + "/explore/repos")
+ })
+ m.Get("/repos", explore.Repos)
+ m.Get("/users", explore.Users)
+ m.Get("/organizations", explore.Organizations)
+ m.Get("/code", explore.Code)
+ }, ignExploreSignIn)
+ m.Get("/issues", reqSignIn, user.Issues)
+ m.Get("/pulls", reqSignIn, user.Pulls)
+ m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
+
+ // ***** START: User *****
+ m.Group("/user", func() {
+ m.Get("/login", user.SignIn)
+ m.Post("/login", bindIgnErr(forms.SignInForm{}), user.SignInPost)
+ m.Group("", func() {
+ m.Combo("/login/openid").
+ Get(user.SignInOpenID).
+ Post(bindIgnErr(forms.SignInOpenIDForm{}), user.SignInOpenIDPost)
+ }, openIDSignInEnabled)
+ m.Group("/openid", func() {
+ m.Combo("/connect").
+ Get(user.ConnectOpenID).
+ Post(bindIgnErr(forms.ConnectOpenIDForm{}), user.ConnectOpenIDPost)
+ m.Group("/register", func() {
+ m.Combo("").
+ Get(user.RegisterOpenID, openIDSignUpEnabled).
+ Post(bindIgnErr(forms.SignUpOpenIDForm{}), user.RegisterOpenIDPost)
+ }, openIDSignUpEnabled)
+ }, openIDSignInEnabled)
+ m.Get("/sign_up", user.SignUp)
+ m.Post("/sign_up", bindIgnErr(forms.RegisterForm{}), user.SignUpPost)
+ m.Group("/oauth2", func() {
+ m.Get("/{provider}", user.SignInOAuth)
+ m.Get("/{provider}/callback", user.SignInOAuthCallback)
+ })
+ m.Get("/link_account", user.LinkAccount)
+ m.Post("/link_account_signin", bindIgnErr(forms.SignInForm{}), user.LinkAccountPostSignIn)
+ m.Post("/link_account_signup", bindIgnErr(forms.RegisterForm{}), user.LinkAccountPostRegister)
+ m.Group("/two_factor", func() {
+ m.Get("", user.TwoFactor)
+ m.Post("", bindIgnErr(forms.TwoFactorAuthForm{}), user.TwoFactorPost)
+ m.Get("/scratch", user.TwoFactorScratch)
+ m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
+ })
+ m.Group("/u2f", func() {
+ m.Get("", user.U2F)
+ m.Get("/challenge", user.U2FChallenge)
+ m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
+
+ })
+ }, reqSignOut)
+
+ m.Any("/user/events", events.Events)
+
+ m.Group("/login/oauth", func() {
+ m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
+ m.Post("/grant", bindIgnErr(forms.GrantApplicationForm{}), user.GrantApplicationOAuth)
+ // TODO manage redirection
+ m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
+ }, ignSignInAndCsrf, reqSignIn)
+ m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
+ m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
+
+ m.Group("/user/settings", func() {
+ m.Get("", userSetting.Profile)
+ m.Post("", bindIgnErr(forms.UpdateProfileForm{}), userSetting.ProfilePost)
+ m.Get("/change_password", user.MustChangePassword)
+ m.Post("/change_password", bindIgnErr(forms.MustChangePasswordForm{}), user.MustChangePasswordPost)
+ m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), userSetting.AvatarPost)
+ m.Post("/avatar/delete", userSetting.DeleteAvatar)
+ m.Group("/account", func() {
+ m.Combo("").Get(userSetting.Account).Post(bindIgnErr(forms.ChangePasswordForm{}), userSetting.AccountPost)
+ m.Post("/email", bindIgnErr(forms.AddEmailForm{}), userSetting.EmailPost)
+ m.Post("/email/delete", userSetting.DeleteEmail)
+ m.Post("/delete", userSetting.DeleteAccount)
+ m.Post("/theme", bindIgnErr(forms.UpdateThemeForm{}), userSetting.UpdateUIThemePost)
+ })
+ m.Group("/security", func() {
+ m.Get("", userSetting.Security)
+ m.Group("/two_factor", func() {
+ m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor)
+ m.Post("/disable", userSetting.DisableTwoFactor)
+ m.Get("/enroll", userSetting.EnrollTwoFactor)
+ m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
+ })
+ m.Group("/u2f", func() {
+ m.Post("/request_register", bindIgnErr(forms.U2FRegistrationForm{}), userSetting.U2FRegister)
+ m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
+ m.Post("/delete", bindIgnErr(forms.U2FDeleteForm{}), userSetting.U2FDelete)
+ })
+ m.Group("/openid", func() {
+ m.Post("", bindIgnErr(forms.AddOpenIDForm{}), userSetting.OpenIDPost)
+ m.Post("/delete", userSetting.DeleteOpenID)
+ m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility)
+ }, openIDSignInEnabled)
+ m.Post("/account_link", userSetting.DeleteAccountLink)
+ })
+ m.Group("/applications/oauth2", func() {
+ m.Get("/{id}", userSetting.OAuth2ApplicationShow)
+ m.Post("/{id}", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
+ m.Post("/{id}/regenerate_secret", userSetting.OAuthApplicationsRegenerateSecret)
+ m.Post("", bindIgnErr(forms.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
+ m.Post("/delete", userSetting.DeleteOAuth2Application)
+ m.Post("/revoke", userSetting.RevokeOAuth2Grant)
+ })
+ m.Combo("/applications").Get(userSetting.Applications).
+ Post(bindIgnErr(forms.NewAccessTokenForm{}), userSetting.ApplicationsPost)
+ m.Post("/applications/delete", userSetting.DeleteApplication)
+ m.Combo("/keys").Get(userSetting.Keys).
+ Post(bindIgnErr(forms.AddKeyForm{}), userSetting.KeysPost)
+ m.Post("/keys/delete", userSetting.DeleteKey)
+ m.Get("/organization", userSetting.Organization)
+ m.Get("/repos", userSetting.Repos)
+ m.Post("/repos/unadopted", userSetting.AdoptOrDeleteRepository)
+ }, reqSignIn, func(ctx *context.Context) {
+ ctx.Data["PageIsUserSettings"] = true
+ ctx.Data["AllThemes"] = setting.UI.Themes
+ })
+
+ m.Group("/user", func() {
+ // r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
+ m.Get("/activate", user.Activate, reqSignIn)
+ m.Post("/activate", user.ActivatePost, reqSignIn)
+ m.Any("/activate_email", user.ActivateEmail)
+ m.Get("/avatar/{username}/{size}", user.Avatar)
+ m.Get("/email2user", user.Email2User)
+ m.Get("/recover_account", user.ResetPasswd)
+ m.Post("/recover_account", user.ResetPasswdPost)
+ m.Get("/forgot_password", user.ForgotPasswd)
+ m.Post("/forgot_password", user.ForgotPasswdPost)
+ m.Post("/logout", user.SignOut)
+ m.Get("/task/{task}", user.TaskStatus)
+ })
+ // ***** END: User *****
+
+ m.Get("/avatar/{hash}", user.AvatarByEmailHash)
+
+ adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
+
+ // ***** START: Admin *****
+ m.Group("/admin", func() {
+ m.Get("", adminReq, admin.Dashboard)
+ m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost)
+ m.Get("/config", admin.Config)
+ m.Post("/config/test_mail", admin.SendTestMail)
+ m.Group("/monitor", func() {
+ m.Get("", admin.Monitor)
+ m.Post("/cancel/{pid}", admin.MonitorCancel)
+ m.Group("/queue/{qid}", func() {
+ m.Get("", admin.Queue)
+ m.Post("/set", admin.SetQueueSettings)
+ m.Post("/add", admin.AddWorkers)
+ m.Post("/cancel/{pid}", admin.WorkerCancel)
+ m.Post("/flush", admin.Flush)
+ })
+ })
+
+ m.Group("/users", func() {
+ m.Get("", admin.Users)
+ m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(forms.AdminCreateUserForm{}), admin.NewUserPost)
+ m.Combo("/{userid}").Get(admin.EditUser).Post(bindIgnErr(forms.AdminEditUserForm{}), admin.EditUserPost)
+ m.Post("/{userid}/delete", admin.DeleteUser)
+ })
+
+ m.Group("/emails", func() {
+ m.Get("", admin.Emails)
+ m.Post("/activate", admin.ActivateEmail)
+ })
+
+ m.Group("/orgs", func() {
+ m.Get("", admin.Organizations)
+ })
+
+ m.Group("/repos", func() {
+ m.Get("", admin.Repos)
+ m.Combo("/unadopted").Get(admin.UnadoptedRepos).Post(admin.AdoptOrDeleteRepository)
+ m.Post("/delete", admin.DeleteRepo)
+ })
+
+ m.Group("/hooks", func() {
+ m.Get("", admin.DefaultOrSystemWebhooks)
+ m.Post("/delete", admin.DeleteDefaultOrSystemWebhook)
+ m.Get("/{id}", repo.WebHooksEdit)
+ m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+ m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ }, webhooksEnabled)
+
+ m.Group("/{configType:default-hooks|system-hooks}", func() {
+ m.Get("/{type}/new", repo.WebhooksNew)
+ m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ })
+
+ m.Group("/auths", func() {
+ m.Get("", admin.Authentications)
+ m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
+ m.Combo("/{authid}").Get(admin.EditAuthSource).
+ Post(bindIgnErr(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
+ m.Post("/{authid}/delete", admin.DeleteAuthSource)
+ })
+
+ m.Group("/notices", func() {
+ m.Get("", admin.Notices)
+ m.Post("/delete", admin.DeleteNotices)
+ m.Post("/empty", admin.EmptyNotices)
+ })
+ }, adminReq)
+ // ***** END: Admin *****
+
+ m.Group("", func() {
+ m.Get("/{username}", user.Profile)
+ m.Get("/attachments/{uuid}", repo.GetAttachment)
+ }, ignSignIn)
+
+ m.Group("/{username}", func() {
+ m.Post("/action/{action}", user.Action)
+ }, reqSignIn)
+
+ if !setting.IsProd() {
+ m.Get("/template/*", dev.TemplatePreview)
+ }
+
+ reqRepoAdmin := context.RequireRepoAdmin()
+ reqRepoCodeWriter := context.RequireRepoWriter(models.UnitTypeCode)
+ reqRepoCodeReader := context.RequireRepoReader(models.UnitTypeCode)
+ reqRepoReleaseWriter := context.RequireRepoWriter(models.UnitTypeReleases)
+ reqRepoReleaseReader := context.RequireRepoReader(models.UnitTypeReleases)
+ reqRepoWikiWriter := context.RequireRepoWriter(models.UnitTypeWiki)
+ reqRepoIssueWriter := context.RequireRepoWriter(models.UnitTypeIssues)
+ reqRepoIssueReader := context.RequireRepoReader(models.UnitTypeIssues)
+ reqRepoPullsReader := context.RequireRepoReader(models.UnitTypePullRequests)
+ reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
+ reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
+ reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects)
+ reqRepoProjectsWriter := context.RequireRepoWriter(models.UnitTypeProjects)
+
+ // ***** START: Organization *****
+ m.Group("/org", func() {
+ m.Group("", func() {
+ m.Get("/create", org.Create)
+ m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
+ })
+
+ m.Group("/{org}", func() {
+ m.Get("/dashboard", user.Dashboard)
+ m.Get("/dashboard/{team}", user.Dashboard)
+ m.Get("/issues", user.Issues)
+ m.Get("/issues/{team}", user.Issues)
+ m.Get("/pulls", user.Pulls)
+ m.Get("/pulls/{team}", user.Pulls)
+ m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
+ m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
+ m.Get("/members", org.Members)
+ m.Post("/members/action/{action}", org.MembersAction)
+ m.Get("/teams", org.Teams)
+ }, context.OrgAssignment(true, false, true))
+
+ m.Group("/{org}", func() {
+ m.Get("/teams/{team}", org.TeamMembers)
+ m.Get("/teams/{team}/repositories", org.TeamRepositories)
+ m.Post("/teams/{team}/action/{action}", org.TeamsAction)
+ m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
+ }, context.OrgAssignment(true, false, true))
+
+ m.Group("/{org}", func() {
+ m.Get("/teams/new", org.NewTeam)
+ m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
+ m.Get("/teams/{team}/edit", org.EditTeam)
+ m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
+ m.Post("/teams/{team}/delete", org.DeleteTeam)
+
+ m.Group("/settings", func() {
+ m.Combo("").Get(org.Settings).
+ Post(bindIgnErr(forms.UpdateOrgSettingForm{}), org.SettingsPost)
+ m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), org.SettingsAvatar)
+ m.Post("/avatar/delete", org.SettingsDeleteAvatar)
+
+ m.Group("/hooks", func() {
+ m.Get("", org.Webhooks)
+ m.Post("/delete", org.DeleteWebhook)
+ m.Get("/{type}/new", repo.WebhooksNew)
+ m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ m.Get("/{id}", repo.WebHooksEdit)
+ m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+ m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ }, webhooksEnabled)
+
+ m.Group("/labels", func() {
+ m.Get("", org.RetrieveLabels, org.Labels)
+ m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), org.NewLabel)
+ m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), org.UpdateLabel)
+ m.Post("/delete", org.DeleteLabel)
+ m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), org.InitializeLabels)
+ })
+
+ m.Route("/delete", "GET,POST", org.SettingsDelete)
+ })
+ }, context.OrgAssignment(true, true))
+ }, reqSignIn)
+ // ***** END: Organization *****
+
+ // ***** START: Repository *****
+ m.Group("/repo", func() {
+ m.Get("/create", repo.Create)
+ m.Post("/create", bindIgnErr(forms.CreateRepoForm{}), repo.CreatePost)
+ m.Get("/migrate", repo.Migrate)
+ m.Post("/migrate", bindIgnErr(forms.MigrateRepoForm{}), repo.MigratePost)
+ m.Group("/fork", func() {
+ m.Combo("/{repoid}").Get(repo.Fork).
+ Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
+ }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
+ }, reqSignIn)
+
+ // ***** Release Attachment Download without Signin
+ m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload)
+
+ m.Group("/{username}/{reponame}", func() {
+ m.Group("/settings", func() {
+ m.Combo("").Get(repo.Settings).
+ Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost)
+ m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar)
+ m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
+
+ m.Group("/collaboration", func() {
+ m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost)
+ m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
+ m.Post("/delete", repo.DeleteCollaboration)
+ m.Group("/team", func() {
+ m.Post("", repo.AddTeamPost)
+ m.Post("/delete", repo.DeleteTeam)
+ })
+ })
+ m.Group("/branches", func() {
+ m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
+ m.Combo("/*").Get(repo.SettingsProtectedBranch).
+ Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
+ }, repo.MustBeNotEmpty)
+
+ m.Group("/hooks/git", func() {
+ m.Get("", repo.GitHooks)
+ m.Combo("/{name}").Get(repo.GitHooksEdit).
+ Post(repo.GitHooksEditPost)
+ }, context.GitHookService())
+
+ m.Group("/hooks", func() {
+ m.Get("", repo.Webhooks)
+ m.Post("/delete", repo.DeleteWebhook)
+ m.Get("/{type}/new", repo.WebhooksNew)
+ m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ m.Get("/{id}", repo.WebHooksEdit)
+ m.Post("/{id}/test", repo.TestWebhook)
+ m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
+ m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ }, webhooksEnabled)
+
+ m.Group("/keys", func() {
+ m.Combo("").Get(repo.DeployKeys).
+ Post(bindIgnErr(forms.AddKeyForm{}), repo.DeployKeysPost)
+ m.Post("/delete", repo.DeleteDeployKey)
+ })
+
+ m.Group("/lfs", func() {
+ m.Get("/", repo.LFSFiles)
+ m.Get("/show/{oid}", repo.LFSFileGet)
+ m.Post("/delete/{oid}", repo.LFSDelete)
+ m.Get("/pointers", repo.LFSPointerFiles)
+ m.Post("/pointers/associate", repo.LFSAutoAssociate)
+ m.Get("/find", repo.LFSFileFind)
+ m.Group("/locks", func() {
+ m.Get("/", repo.LFSLocks)
+ m.Post("/", repo.LFSLockFile)
+ m.Post("/{lid}/unlock", repo.LFSUnlock)
+ })
+ })
+
+ }, func(ctx *context.Context) {
+ ctx.Data["PageIsSettings"] = true
+ ctx.Data["LFSStartServer"] = setting.LFS.StartServer
+ })
+ }, reqSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoAdmin, context.RepoRef())
+
+ m.Post("/{username}/{reponame}/action/{action}", reqSignIn, context.RepoAssignment, context.UnitTypes(), repo.Action)
+
+ // Grouping for those endpoints not requiring authentication
+ m.Group("/{username}/{reponame}", func() {
+ m.Group("/milestone", func() {
+ m.Get("/{id}", repo.MilestoneIssuesAndPulls)
+ }, reqRepoIssuesOrPullsReader, context.RepoRef())
+ m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
+ Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
+ Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
+ }, context.RepoAssignment, context.UnitTypes())
+
+ // Grouping for those endpoints that do require authentication
+ m.Group("/{username}/{reponame}", func() {
+ m.Group("/issues", func() {
+ m.Group("/new", func() {
+ m.Combo("").Get(context.RepoRef(), repo.NewIssue).
+ Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
+ m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
+ })
+ }, context.RepoMustNotBeArchived(), reqRepoIssueReader)
+ // FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
+ // So they can apply their own enable/disable logic on routers.
+ m.Group("/issues", func() {
+ m.Group("/{index}", func() {
+ m.Post("/title", repo.UpdateIssueTitle)
+ m.Post("/content", repo.UpdateIssueContent)
+ m.Post("/watch", repo.IssueWatch)
+ m.Post("/ref", repo.UpdateIssueRef)
+ m.Group("/dependency", func() {
+ m.Post("/add", repo.AddDependency)
+ m.Post("/delete", repo.RemoveDependency)
+ })
+ m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
+ m.Group("/times", func() {
+ m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
+ m.Post("/{timeid}/delete", repo.DeleteTime)
+ m.Group("/stopwatch", func() {
+ m.Post("/toggle", repo.IssueStopwatch)
+ m.Post("/cancel", repo.CancelStopwatch)
+ })
+ })
+ m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction)
+ m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue)
+ m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
+ }, context.RepoMustNotBeArchived())
+ m.Group("/{index}", func() {
+ m.Get("/attachments", repo.GetIssueAttachments)
+ m.Get("/attachments/{uuid}", repo.GetAttachment)
+ })
+
+ m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
+ m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
+ m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
+ m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
+ m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
+ m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(forms.DismissReviewForm{}), repo.DismissReview)
+ m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
+ m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
+ m.Post("/attachments", repo.UploadIssueAttachment)
+ m.Post("/attachments/remove", repo.DeleteAttachment)
+ }, context.RepoMustNotBeArchived())
+ m.Group("/comments/{id}", func() {
+ m.Post("", repo.UpdateCommentContent)
+ m.Post("/delete", repo.DeleteComment)
+ m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeCommentReaction)
+ }, context.RepoMustNotBeArchived())
+ m.Group("/comments/{id}", func() {
+ m.Get("/attachments", repo.GetCommentAttachments)
+ })
+ m.Group("/labels", func() {
+ m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel)
+ m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel)
+ m.Post("/delete", repo.DeleteLabel)
+ m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), repo.InitializeLabels)
+ }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
+ m.Group("/milestones", func() {
+ m.Combo("/new").Get(repo.NewMilestone).
+ Post(bindIgnErr(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
+ m.Get("/{id}/edit", repo.EditMilestone)
+ m.Post("/{id}/edit", bindIgnErr(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
+ m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
+ m.Post("/delete", repo.DeleteMilestone)
+ }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
+ m.Group("/pull", func() {
+ m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget)
+ }, context.RepoMustNotBeArchived())
+
+ m.Group("", func() {
+ m.Group("", func() {
+ m.Combo("/_edit/*").Get(repo.EditFile).
+ Post(bindIgnErr(forms.EditRepoFileForm{}), repo.EditFilePost)
+ m.Combo("/_new/*").Get(repo.NewFile).
+ Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewFilePost)
+ m.Post("/_preview/*", bindIgnErr(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
+ m.Combo("/_delete/*").Get(repo.DeleteFile).
+ Post(bindIgnErr(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
+ m.Combo("/_upload/*", repo.MustBeAbleToUpload).
+ Get(repo.UploadFile).
+ Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
+ }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
+ m.Group("", func() {
+ m.Post("/upload-file", repo.UploadFileToServer)
+ m.Post("/upload-remove", bindIgnErr(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+ }, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
+ }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+ m.Group("/branches", func() {
+ m.Group("/_new", func() {
+ m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
+ m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
+ m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
+ }, bindIgnErr(forms.NewBranchForm{}))
+ m.Post("/delete", repo.DeleteBranchPost)
+ m.Post("/restore", repo.RestoreBranchPost)
+ }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+ }, reqSignIn, context.RepoAssignment, context.UnitTypes())
+
+ // Releases
+ m.Group("/{username}/{reponame}", func() {
+ m.Get("/tags", repo.TagsList, repo.MustBeNotEmpty,
+ reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag))
+ m.Group("/releases", func() {
+ m.Get("/", repo.Releases)
+ m.Get("/tag/*", repo.SingleRelease)
+ m.Get("/latest", repo.LatestRelease)
+ }, repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
+ m.Get("/releases/attachments/{uuid}", repo.GetAttachment, repo.MustBeNotEmpty, reqRepoReleaseReader)
+ m.Group("/releases", func() {
+ m.Get("/new", repo.NewRelease)
+ m.Post("/new", bindIgnErr(forms.NewReleaseForm{}), repo.NewReleasePost)
+ m.Post("/delete", repo.DeleteRelease)
+ m.Post("/attachments", repo.UploadReleaseAttachment)
+ m.Post("/attachments/remove", repo.DeleteAttachment)
+ }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
+ m.Post("/tags/delete", repo.DeleteTag, reqSignIn,
+ repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
+ m.Group("/releases", func() {
+ m.Get("/edit/*", repo.EditRelease)
+ m.Post("/edit/*", bindIgnErr(forms.EditReleaseForm{}), repo.EditReleasePost)
+ }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) {
+ var err error
+ ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+ if err != nil {
+ ctx.ServerError("GetBranchCommit", err)
+ return
+ }
+ ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
+ if err != nil {
+ ctx.ServerError("GetCommitsCount", err)
+ return
+ }
+ ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
+ })
+ m.Get("/attachments/{uuid}", repo.GetAttachment)
+ }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
+
+ m.Group("/{username}/{reponame}", func() {
+ m.Post("/topics", repo.TopicsPost)
+ }, context.RepoAssignment, context.RepoMustNotBeArchived(), reqRepoAdmin)
+
+ m.Group("/{username}/{reponame}", func() {
+ m.Group("", func() {
+ m.Get("/{type:issues|pulls}", repo.Issues)
+ m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue)
+ m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels)
+ m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones)
+ }, context.RepoRef())
+
+ m.Group("/projects", func() {
+ m.Get("", repo.Projects)
+ m.Get("/{id}", repo.ViewProject)
+ m.Group("", func() {
+ m.Get("/new", repo.NewProject)
+ m.Post("/new", bindIgnErr(forms.CreateProjectForm{}), repo.NewProjectPost)
+ m.Group("/{id}", func() {
+ m.Post("", bindIgnErr(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
+ m.Post("/delete", repo.DeleteProject)
+
+ m.Get("/edit", repo.EditProject)
+ m.Post("/edit", bindIgnErr(forms.CreateProjectForm{}), repo.EditProjectPost)
+ m.Post("/{action:open|close}", repo.ChangeProjectStatus)
+
+ m.Group("/{boardID}", func() {
+ m.Put("", bindIgnErr(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
+ m.Delete("", repo.DeleteProjectBoard)
+ m.Post("/default", repo.SetDefaultProjectBoard)
+
+ m.Post("/{index}", repo.MoveIssueAcrossBoards)
+ })
+ })
+ }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
+ }, reqRepoProjectsReader, repo.MustEnableProjects)
+
+ m.Group("/wiki", func() {
+ m.Get("/", repo.Wiki)
+ m.Get("/{page}", repo.Wiki)
+ m.Get("/_pages", repo.WikiPages)
+ m.Get("/{page}/_revision", repo.WikiRevision)
+ m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+ m.Get("/commit/{sha:[a-f0-9]{7,40}}.{:patch|diff}", repo.RawDiff)
+
+ m.Group("", func() {
+ m.Combo("/_new").Get(repo.NewWiki).
+ Post(bindIgnErr(forms.NewWikiForm{}), repo.NewWikiPost)
+ m.Combo("/{page}/_edit").Get(repo.EditWiki).
+ Post(bindIgnErr(forms.NewWikiForm{}), repo.EditWikiPost)
+ m.Post("/{page}/delete", repo.DeleteWikiPagePost)
+ }, context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter)
+ }, repo.MustEnableWiki, context.RepoRef(), func(ctx *context.Context) {
+ ctx.Data["PageIsWiki"] = true
+ })
+
+ m.Group("/wiki", func() {
+ m.Get("/raw/*", repo.WikiRaw)
+ }, repo.MustEnableWiki)
+
+ m.Group("/activity", func() {
+ m.Get("", repo.Activity)
+ m.Get("/{period}", repo.Activity)
+ }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))
+
+ m.Group("/activity_author_data", func() {
+ m.Get("", repo.ActivityAuthors)
+ m.Get("/{period}", repo.ActivityAuthors)
+ }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
+
+ m.Group("/archive", func() {
+ m.Get("/*", common.Download)
+ m.Post("/*", repo.InitiateDownload)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+ m.Group("/branches", func() {
+ m.Get("", repo.Branches)
+ }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+ m.Group("/blob_excerpt", func() {
+ m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob)
+ }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+ m.Group("/pulls/{index}", func() {
+ m.Get(".diff", repo.DownloadPullDiff)
+ m.Get(".patch", repo.DownloadPullPatch)
+ m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
+ m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
+ m.Post("/update", repo.UpdatePullRequest)
+ m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
+ m.Group("/files", func() {
+ m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
+ m.Group("/reviews", func() {
+ m.Get("/new_comment", repo.RenderNewCodeCommentForm)
+ m.Post("/comments", bindIgnErr(forms.CodeCommentForm{}), repo.CreateCodeComment)
+ m.Post("/submit", bindIgnErr(forms.SubmitReviewForm{}), repo.SubmitReview)
+ }, context.RepoMustNotBeArchived())
+ })
+ }, repo.MustAllowPulls)
+
+ m.Group("/media", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS)
+ m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS)
+ // "/*" route is deprecated, and kept for backward compatibility
+ m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownloadOrLFS)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+ m.Group("/raw", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
+ m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID)
+ // "/*" route is deprecated, and kept for backward compatibility
+ m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+ m.Group("/commits", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits)
+ // "/*" route is deprecated, and kept for backward compatibility
+ m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+ m.Group("/blame", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
+ m.Group("", func() {
+ m.Get("/graph", repo.Graph)
+ m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
+ }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
+
+ m.Group("/src", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
+ // "/*" route is deprecated, and kept for backward compatibility
+ m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
+ }, repo.SetEditorconfigIfExists)
+
+ m.Group("", func() {
+ m.Get("/forks", repo.Forks)
+ }, context.RepoRef(), reqRepoCodeReader)
+ m.Get("/commit/{sha:([a-f0-9]{7,40})}.{ext:patch|diff}",
+ repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
+ }, ignSignIn, context.RepoAssignment, context.UnitTypes())
+ m.Group("/{username}/{reponame}", func() {
+ m.Get("/stars", repo.Stars)
+ m.Get("/watchers", repo.Watchers)
+ m.Get("/search", reqRepoCodeReader, repo.Search)
+ }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
+
+ m.Group("/{username}", func() {
+ m.Group("/{reponame}", func() {
+ m.Get("", repo.SetEditorconfigIfExists, repo.Home)
+ }, ignSignIn, context.RepoAssignment, context.RepoRef(), context.UnitTypes())
+
+ m.Group("/{reponame}", func() {
+ m.Group("/info/lfs", func() {
+ m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
+ m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
+ m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
+ m.Get("/objects/{oid}", lfs.DownloadHandler)
+ m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
+ m.Group("/locks", func() {
+ m.Get("/", lfs.GetListLockHandler)
+ m.Post("/", lfs.PostLockHandler)
+ m.Post("/verify", lfs.VerifyLockHandler)
+ m.Post("/{lid}/unlock", lfs.UnLockHandler)
+ }, lfs.CheckAcceptMediaType)
+ m.Any("/*", func(ctx *context.Context) {
+ ctx.NotFound("", nil)
+ })
+ }, ignSignInAndCsrf, lfsServerEnabled)
+
+ m.Group("", func() {
+ m.Post("/git-upload-pack", repo.ServiceUploadPack)
+ m.Post("/git-receive-pack", repo.ServiceReceivePack)
+ m.Get("/info/refs", repo.GetInfoRefs)
+ m.Get("/HEAD", repo.GetTextFile("HEAD"))
+ m.Get("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
+ m.Get("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
+ m.Get("/objects/info/packs", repo.GetInfoPacks)
+ m.Get("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
+ m.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
+ m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
+ m.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
+ }, ignSignInAndCsrf)
+
+ m.Head("/tasks/trigger", repo.TriggerTask)
+ })
+ })
+ // ***** END: Repository *****
+
+ m.Group("/notifications", func() {
+ m.Get("", user.Notifications)
+ m.Post("/status", user.NotificationStatusPost)
+ m.Post("/purge", user.NotificationPurgePost)
+ }, reqSignIn)
+
+ if setting.API.EnableSwagger {
+ m.Get("/swagger.v1.json", SwaggerV1Json)
+ }
+}