diff options
Diffstat (limited to 'routers')
-rw-r--r-- | routers/admin/admin.go | 168 | ||||
-rw-r--r-- | routers/admin/user.go | 144 | ||||
-rw-r--r-- | routers/api/v1/miscellaneous.go | 18 | ||||
-rw-r--r-- | routers/dashboard.go | 40 | ||||
-rw-r--r-- | routers/dev/template.go | 25 | ||||
-rw-r--r-- | routers/install.go | 201 | ||||
-rw-r--r-- | routers/repo/branch.go | 28 | ||||
-rw-r--r-- | routers/repo/commit.go | 92 | ||||
-rw-r--r-- | routers/repo/git.go | 55 | ||||
-rw-r--r-- | routers/repo/http.go | 471 | ||||
-rw-r--r-- | routers/repo/issue.go | 296 | ||||
-rw-r--r-- | routers/repo/pull.go | 16 | ||||
-rw-r--r-- | routers/repo/release.go | 30 | ||||
-rw-r--r-- | routers/repo/repo.go | 426 | ||||
-rw-r--r-- | routers/user/setting.go | 184 | ||||
-rw-r--r-- | routers/user/social.go | 159 | ||||
-rw-r--r-- | routers/user/user.go | 514 |
17 files changed, 2867 insertions, 0 deletions
diff --git a/routers/admin/admin.go b/routers/admin/admin.go new file mode 100644 index 0000000000..18a43ff817 --- /dev/null +++ b/routers/admin/admin.go @@ -0,0 +1,168 @@ +// 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 ( + "fmt" + "runtime" + "strings" + "time" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +var startTime = time.Now() + +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 = base.TimeSincePro(startTime) + + 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 +} + +func Dashboard(ctx *middleware.Context) { + ctx.Data["Title"] = "Admin Dashboard" + ctx.Data["PageIsDashboard"] = true + ctx.Data["Stats"] = models.GetStatistic() + updateSystemStatus() + ctx.Data["SysStatus"] = sysStatus + ctx.HTML(200, "admin/dashboard") +} + +func Users(ctx *middleware.Context) { + ctx.Data["Title"] = "User Management" + ctx.Data["PageIsUsers"] = true + + var err error + ctx.Data["Users"], err = models.GetUsers(100, 0) + if err != nil { + ctx.Handle(200, "admin.Users", err) + return + } + ctx.HTML(200, "admin/users") +} + +func Repositories(ctx *middleware.Context) { + ctx.Data["Title"] = "Repository Management" + ctx.Data["PageIsRepos"] = true + + var err error + ctx.Data["Repos"], err = models.GetRepos(100, 0) + if err != nil { + ctx.Handle(200, "admin.Repositories", err) + return + } + ctx.HTML(200, "admin/repos") +} + +func Config(ctx *middleware.Context) { + ctx.Data["Title"] = "Server Configuration" + ctx.Data["PageIsConfig"] = true + + ctx.Data["AppUrl"] = base.AppUrl + ctx.Data["Domain"] = base.Domain + ctx.Data["RunUser"] = base.RunUser + ctx.Data["RunMode"] = strings.Title(martini.Env) + ctx.Data["RepoRootPath"] = base.RepoRootPath + + ctx.Data["Service"] = base.Service + + ctx.Data["DbCfg"] = models.DbCfg + + ctx.Data["MailerEnabled"] = false + if base.MailService != nil { + ctx.Data["MailerEnabled"] = true + ctx.Data["Mailer"] = base.MailService + } + + ctx.Data["CacheAdapter"] = base.CacheAdapter + ctx.Data["CacheConfig"] = base.CacheConfig + + ctx.Data["SessionProvider"] = base.SessionProvider + ctx.Data["SessionConfig"] = base.SessionConfig + + ctx.Data["PictureService"] = base.PictureService + + ctx.Data["LogMode"] = base.LogMode + ctx.Data["LogConfig"] = base.LogConfig + + ctx.HTML(200, "admin/config") +} diff --git a/routers/admin/user.go b/routers/admin/user.go new file mode 100644 index 0000000000..9f043507d1 --- /dev/null +++ b/routers/admin/user.go @@ -0,0 +1,144 @@ +// 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 ( + "strings" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" +) + +func NewUser(ctx *middleware.Context, form auth.RegisterForm) { + ctx.Data["Title"] = "New Account" + ctx.Data["PageIsUsers"] = true + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "admin/users/new") + return + } + + if form.Password != form.RetypePasswd { + ctx.Data["HasError"] = true + ctx.Data["Err_Password"] = true + ctx.Data["Err_RetypePasswd"] = true + ctx.Data["ErrorMsg"] = "Password and re-type password are not same" + auth.AssignForm(form, ctx.Data) + } + + if ctx.HasError() { + ctx.HTML(200, "admin/users/new") + return + } + + u := &models.User{ + Name: form.UserName, + Email: form.Email, + Passwd: form.Password, + IsActive: true, + } + + var err error + if u, err = models.RegisterUser(u); err != nil { + switch err { + case models.ErrUserAlreadyExist: + ctx.RenderWithErr("Username has been already taken", "admin/users/new", &form) + case models.ErrEmailAlreadyUsed: + ctx.RenderWithErr("E-mail address has been already used", "admin/users/new", &form) + case models.ErrUserNameIllegal: + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "admin/users/new", &form) + default: + ctx.Handle(200, "admin.user.NewUser", err) + } + return + } + + log.Trace("%s User created by admin(%s): %s", ctx.Req.RequestURI, + ctx.User.LowerName, strings.ToLower(form.UserName)) + + ctx.Redirect("/admin/users") +} + +func EditUser(ctx *middleware.Context, params martini.Params, form auth.AdminEditUserForm) { + ctx.Data["Title"] = "Edit Account" + ctx.Data["PageIsUsers"] = true + + uid, err := base.StrTo(params["userid"]).Int() + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + u, err := models.GetUserById(int64(uid)) + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + if ctx.Req.Method == "GET" { + ctx.Data["User"] = u + ctx.HTML(200, "admin/users/edit") + return + } + + u.Email = form.Email + u.Website = form.Website + u.Location = form.Location + u.Avatar = base.EncodeMd5(form.Avatar) + u.AvatarEmail = form.Avatar + u.IsActive = form.Active == "on" + u.IsAdmin = form.Admin == "on" + if err := models.UpdateUser(u); err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + ctx.Data["IsSuccess"] = true + ctx.Data["User"] = u + ctx.HTML(200, "admin/users/edit") + + log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI, + ctx.User.LowerName, ctx.User.LowerName) +} + +func DeleteUser(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Edit Account" + ctx.Data["PageIsUsers"] = true + + uid, err := base.StrTo(params["userid"]).Int() + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + u, err := models.GetUserById(int64(uid)) + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + if err = models.DeleteUser(u); err != nil { + ctx.Data["HasError"] = true + switch err { + case models.ErrUserOwnRepos: + ctx.Data["ErrorMsg"] = "This account still has ownership of repository, owner has to delete or transfer them first." + ctx.Data["User"] = u + ctx.HTML(200, "admin/users/edit") + default: + ctx.Handle(200, "admin.user.DeleteUser", err) + } + return + } + + log.Trace("%s User deleted by admin(%s): %s", ctx.Req.RequestURI, + ctx.User.LowerName, ctx.User.LowerName) + + ctx.Redirect("/admin/users") +} diff --git a/routers/api/v1/miscellaneous.go b/routers/api/v1/miscellaneous.go new file mode 100644 index 0000000000..babdfce9b2 --- /dev/null +++ b/routers/api/v1/miscellaneous.go @@ -0,0 +1,18 @@ +// 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 v1 + +import ( + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func Markdown(ctx *middleware.Context) { + content := ctx.Query("content") + ctx.Render.JSON(200, map[string]interface{}{ + "ok": true, + "content": string(base.RenderMarkdown([]byte(content), ctx.Query("repoLink"))), + }) +} diff --git a/routers/dashboard.go b/routers/dashboard.go new file mode 100644 index 0000000000..2c81cf23c1 --- /dev/null +++ b/routers/dashboard.go @@ -0,0 +1,40 @@ +// 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 ( + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/routers/user" +) + +func Home(ctx *middleware.Context) { + if ctx.IsSigned { + user.Dashboard(ctx) + return + } + + // Check auto-login. + userName := ctx.GetCookie(base.CookieUserName) + if len(userName) != 0 { + ctx.Redirect("/user/login") + return + } + + ctx.Data["PageIsHome"] = true + ctx.HTML(200, "home") +} + +func Help(ctx *middleware.Context) { + ctx.Data["PageIsHelp"] = true + ctx.Data["Title"] = "Help" + ctx.HTML(200, "help") +} + +func NotFound(ctx *middleware.Context) { + ctx.Data["PageIsNotFound"] = true + ctx.Data["Title"] = "Page Not Found" + ctx.Handle(404, "home.NotFound", nil) +} diff --git a/routers/dev/template.go b/routers/dev/template.go new file mode 100644 index 0000000000..63d5d9a5bb --- /dev/null +++ b/routers/dev/template.go @@ -0,0 +1,25 @@ +// 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 ( + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func TemplatePreview(ctx *middleware.Context, params martini.Params) { + ctx.Data["User"] = models.User{Name: "Unknown"} + ctx.Data["AppName"] = base.AppName + ctx.Data["AppVer"] = base.AppVer + ctx.Data["AppUrl"] = base.AppUrl + ctx.Data["AppLogo"] = base.AppLogo + ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374" + ctx.Data["ActiveCodeLives"] = base.Service.ActiveCodeLives / 60 + ctx.Data["ResetPwdCodeLives"] = base.Service.ResetPwdCodeLives / 60 + ctx.HTML(200, params["_1"]) +} diff --git a/routers/install.go b/routers/install.go new file mode 100644 index 0000000000..5d6c65ef9b --- /dev/null +++ b/routers/install.go @@ -0,0 +1,201 @@ +// 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 ( + "errors" + "os" + "os/exec" + "strings" + + "github.com/Unknwon/goconfig" + "github.com/go-martini/martini" + "github.com/lunny/xorm" + qlog "github.com/qiniu/log" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/middleware" +) + +// Check run mode(Default of martini is Dev). +func checkRunMode() { + switch base.Cfg.MustValue("", "RUN_MODE") { + case "prod": + martini.Env = martini.Prod + base.IsProdMode = true + case "test": + martini.Env = martini.Test + } + log.Info("Run Mode: %s", strings.Title(martini.Env)) +} + +// GlobalInit is for global configuration reload-able. +func GlobalInit() { + base.NewConfigContext() + mailer.NewMailerContext() + models.LoadModelsConfig() + models.LoadRepoConfig() + models.NewRepoContext() + + if base.InstallLock { + if err := models.NewEngine(); err != nil { + qlog.Fatal(err) + } + + models.HasEngine = true + } + base.NewServices() + checkRunMode() +} + +func Install(ctx *middleware.Context, form auth.InstallForm) { + if base.InstallLock { + ctx.Handle(404, "install.Install", errors.New("Installation is prohibited")) + return + } + + ctx.Data["Title"] = "Install" + ctx.Data["PageIsInstall"] = true + + if ctx.Req.Method == "GET" { + // Get and assign value to install form. + if len(form.Host) == 0 { + form.Host = models.DbCfg.Host + } + if len(form.User) == 0 { + form.User = models.DbCfg.User + } + if len(form.Passwd) == 0 { + form.Passwd = models.DbCfg.Pwd + } + if len(form.DatabaseName) == 0 { + form.DatabaseName = models.DbCfg.Name + } + if len(form.DatabasePath) == 0 { + form.DatabasePath = models.DbCfg.Path + } + + if len(form.RepoRootPath) == 0 { + form.RepoRootPath = base.RepoRootPath + } + if len(form.RunUser) == 0 { + form.RunUser = base.RunUser + } + if len(form.Domain) == 0 { + form.Domain = base.Domain + } + if len(form.AppUrl) == 0 { + form.AppUrl = base.AppUrl + } + + auth.AssignForm(form, ctx.Data) + ctx.HTML(200, "install") + return + } + + if ctx.HasError() { + ctx.HTML(200, "install") + return + } + + if _, err := exec.LookPath("git"); err != nil { + ctx.RenderWithErr("Fail to test 'git' command: "+err.Error(), "install", &form) + return + } + + // Pass basic check, now test configuration. + // Test database setting. + dbTypes := map[string]string{"mysql": "mysql", "pgsql": "postgres", "sqlite": "sqlite3"} + models.DbCfg.Type = dbTypes[form.Database] + models.DbCfg.Host = form.Host + models.DbCfg.User = form.User + models.DbCfg.Pwd = form.Passwd + models.DbCfg.Name = form.DatabaseName + models.DbCfg.SslMode = form.SslMode + models.DbCfg.Path = form.DatabasePath + + // Set test engine. + var x *xorm.Engine + if err := models.NewTestEngine(x); err != nil { + if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { + ctx.RenderWithErr("Your release version does not support SQLite3, please download the official binary version "+ + "from https://github.com/gogits/gogs/wiki/Install-from-binary, NOT the gobuild version.", "install", &form) + } else { + ctx.RenderWithErr("Database setting is not correct: "+err.Error(), "install", &form) + } + return + } + + // Test repository root path. + if err := os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil { + ctx.RenderWithErr("Repository root path is invalid: "+err.Error(), "install", &form) + return + } + + // Check run user. + curUser := os.Getenv("USERNAME") + if len(curUser) == 0 { + curUser = os.Getenv("USER") + } + // Does not check run user when the install lock is off. + if form.RunUser != curUser { + ctx.RenderWithErr("Run user isn't the current user: "+form.RunUser+" -> "+curUser, "install", &form) + return + } + + // Save settings. + base.Cfg.SetValue("database", "DB_TYPE", models.DbCfg.Type) + base.Cfg.SetValue("database", "HOST", models.DbCfg.Host) + base.Cfg.SetValue("database", "NAME", models.DbCfg.Name) + base.Cfg.SetValue("database", "USER", models.DbCfg.User) + base.Cfg.SetValue("database", "PASSWD", models.DbCfg.Pwd) + base.Cfg.SetValue("database", "SSL_MODE", models.DbCfg.SslMode) + base.Cfg.SetValue("database", "PATH", models.DbCfg.Path) + + base.Cfg.SetValue("repository", "ROOT", form.RepoRootPath) + base.Cfg.SetValue("", "RUN_USER", form.RunUser) + base.Cfg.SetValue("server", "DOMAIN", form.Domain) + base.Cfg.SetValue("server", "ROOT_URL", form.AppUrl) + + if len(strings.TrimSpace(form.SmtpHost)) > 0 { + base.Cfg.SetValue("mailer", "ENABLED", "true") + base.Cfg.SetValue("mailer", "HOST", form.SmtpHost) + base.Cfg.SetValue("mailer", "USER", form.SmtpEmail) + base.Cfg.SetValue("mailer", "PASSWD", form.SmtpPasswd) + + base.Cfg.SetValue("service", "REGISTER_EMAIL_CONFIRM", base.ToStr(form.RegisterConfirm == "on")) + base.Cfg.SetValue("service", "ENABLE_NOTIFY_MAIL", base.ToStr(form.MailNotify == "on")) + } + + base.Cfg.SetValue("", "RUN_MODE", "prod") + + base.Cfg.SetValue("security", "INSTALL_LOCK", "true") + + os.MkdirAll("custom/conf", os.ModePerm) + if err := goconfig.SaveConfigFile(base.Cfg, "custom/conf/app.ini"); err != nil { + ctx.RenderWithErr("Fail to save configuration: "+err.Error(), "install", &form) + return + } + + GlobalInit() + + // Create admin account. + if _, err := models.RegisterUser(&models.User{Name: form.AdminName, Email: form.AdminEmail, Passwd: form.AdminPasswd, + IsAdmin: true, IsActive: true}); err != nil { + if err != models.ErrUserAlreadyExist { + base.InstallLock = false + ctx.RenderWithErr("Admin account setting is invalid: "+err.Error(), "install", &form) + return + } + log.Info("Admin account already exist") + } + + log.Info("First-time run install finished!") + ctx.Redirect("/user/login") +} diff --git a/routers/repo/branch.go b/routers/repo/branch.go new file mode 100644 index 0000000000..ffd118ae14 --- /dev/null +++ b/routers/repo/branch.go @@ -0,0 +1,28 @@ +// 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 ( + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/middleware" +) + +func Branches(ctx *middleware.Context, params martini.Params) { + brs, err := models.GetBranches(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) + if err != nil { + ctx.Handle(404, "repo.Branches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Branches", nil) + return + } + + ctx.Data["Branches"] = brs + ctx.Data["IsRepoToolbarBranches"] = true + + ctx.HTML(200, "repo/branches") +} diff --git a/routers/repo/commit.go b/routers/repo/commit.go new file mode 100644 index 0000000000..d29c40e67e --- /dev/null +++ b/routers/repo/commit.go @@ -0,0 +1,92 @@ +// 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" + "path" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func Commits(ctx *middleware.Context, params martini.Params) { + userName := params["username"] + repoName := params["reponame"] + branchName := params["branchname"] + + brs, err := models.GetBranches(userName, repoName) + if err != nil { + ctx.Handle(200, "repo.Commits", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Commits", nil) + return + } + + var commits *list.List + if models.IsBranchExist(userName, repoName, branchName) { + commits, err = models.GetCommitsByBranch(userName, repoName, branchName) + } else { + commits, err = models.GetCommitsByCommitId(userName, repoName, branchName) + } + + if err != nil { + ctx.Handle(404, "repo.Commits", err) + return + } + + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.Data["IsRepoToolbarCommits"] = true + ctx.HTML(200, "repo/commits") +} + +func Diff(ctx *middleware.Context, params martini.Params) { + userName := ctx.Repo.Owner.Name + repoName := ctx.Repo.Repository.Name + branchName := ctx.Repo.BranchName + commitId := ctx.Repo.CommitId + + commit := ctx.Repo.Commit + + diff, err := models.GetDiff(models.RepoPath(userName, repoName), commitId) + if err != nil { + ctx.Handle(404, "repo.Diff", err) + return + } + + isImageFile := func(name string) bool { + repoFile, err := models.GetTargetFile(userName, repoName, + branchName, commitId, name) + + if err != nil { + return false + } + + blob, err := repoFile.LookupBlob() + if err != nil { + return false + } + + data := blob.Contents() + _, isImage := base.IsImageFile(data) + return isImage + } + + ctx.Data["IsImageFile"] = isImageFile + ctx.Data["Title"] = commit.Message() + " ยท " + base.ShortSha(commitId) + ctx.Data["Commit"] = commit + ctx.Data["Diff"] = diff + ctx.Data["IsRepoToolbarCommits"] = true + ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId) + ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId) + ctx.HTML(200, "repo/diff") +} diff --git a/routers/repo/git.go b/routers/repo/git.go new file mode 100644 index 0000000000..30c1042e0a --- /dev/null +++ b/routers/repo/git.go @@ -0,0 +1,55 @@ +package repo + +import ( + "fmt" + "strings" +) + +const advertise_refs = "--advertise-refs" + +func command(cmd string, opts ...string) string { + return fmt.Sprintf("git %s %s", cmd, strings.Join(opts, " ")) +} + +/*func upload_pack(repository_path string, opts ...string) string { + cmd = "upload-pack" + opts = append(opts, "--stateless-rpc", repository_path) + return command(cmd, opts...) +} + +func receive_pack(repository_path string, opts ...string) string { + cmd = "receive-pack" + opts = append(opts, "--stateless-rpc", repository_path) + return command(cmd, opts...) +}*/ + +/*func update_server_info(repository_path, opts = {}, &block) + cmd = "update-server-info" + args = [] + opts.each {|k,v| args << command_options[k] if command_options.has_key?(k) } + opts[:args] = args + Dir.chdir(repository_path) do # "git update-server-info" does not take a parameter to specify the repository, so set the working directory to the repository + self.command(cmd, opts, &block) + end + end + + def get_config_setting(repository_path, key) + path = get_config_location(repository_path) + raise "Config file could not be found for repository in #{repository_path}." unless path + self.command("config", {:args => ["-f #{path}", key]}).chomp + end + + def get_config_location(repository_path) + non_bare = File.join(repository_path,'.git') # This is where the config file will be if the repository is non-bare + if File.exists?(non_bare) then # The repository is non-bare + non_bare_config = File.join(non_bare, 'config') + return non_bare_config if File.exists?(non_bare_config) + else # We are dealing with a bare repository + bare_config = File.join(repository_path, "config") + return bare_config if File.exists?(bare_config) + end + return nil + end + + end +*/ diff --git a/routers/repo/http.go b/routers/repo/http.go new file mode 100644 index 0000000000..5aa3139f85 --- /dev/null +++ b/routers/repo/http.go @@ -0,0 +1,471 @@ +package repo + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/go-martini/martini" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func Http(ctx *middleware.Context, params martini.Params) { + username := params["username"] + reponame := params["reponame"] + if strings.HasSuffix(reponame, ".git") { + reponame = reponame[:len(reponame)-4] + } + + var isPull bool + service := ctx.Query("service") + if service == "git-receive-pack" || + strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") { + isPull = false + } else if service == "git-upload-pack" || + strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { + isPull = true + } else { + isPull = (ctx.Req.Method == "GET") + } + + repoUser, err := models.GetUserByName(username) + if err != nil { + ctx.Handle(500, "repo.GetUserByName", nil) + return + } + + repo, err := models.GetRepositoryByName(repoUser.Id, reponame) + if err != nil { + ctx.Handle(500, "repo.GetRepositoryByName", nil) + return + } + + // only public pull don't need auth + var askAuth = !(!repo.IsPrivate && isPull) + + // check access + if askAuth { + baHead := ctx.Req.Header.Get("Authorization") + if baHead == "" { + // ask auth + authRequired(ctx) + return + } + + auths := strings.Fields(baHead) + // currently check basic auth + // TODO: support digit auth + if len(auths) != 2 || auths[0] != "Basic" { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + authUsername, passwd, err := basicDecode(auths[1]) + if err != nil { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + + authUser, err := models.GetUserByName(authUsername) + if err != nil { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + + newUser := &models.User{Passwd: passwd, Salt: authUser.Salt} + + newUser.EncodePasswd() + if authUser.Passwd != newUser.Passwd { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + + var tp = models.AU_WRITABLE + if isPull { + tp = models.AU_READABLE + } + + has, err := models.HasAccess(authUsername, username+"/"+reponame, tp) + if err != nil { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } else if !has { + if tp == models.AU_READABLE { + has, err = models.HasAccess(authUsername, username+"/"+reponame, models.AU_WRITABLE) + if err != nil || !has { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + } else { + ctx.Handle(401, "no basic auth and digit auth", nil) + return + } + } + } + + config := Config{base.RepoRootPath, "git", true, true, func(rpc string, input []byte) { + //fmt.Println("rpc:", rpc) + //fmt.Println("input:", string(input)) + }} + + handler := HttpBackend(&config) + handler(ctx.ResponseWriter, ctx.Req) + + /* Webdav + dir := models.RepoPath(username, reponame) + + prefix := path.Join("/", username, params["reponame"]) + server := webdav.NewServer( + dir, prefix, true) + + server.ServeHTTP(ctx.ResponseWriter, ctx.Req) + */ +} + +type route struct { + cr *regexp.Regexp + method string + handler func(handler) +} + +type Config struct { + ReposRoot string + GitBinPath string + UploadPack bool + ReceivePack bool + OnSucceed func(rpc string, input []byte) +} + +type handler struct { + *Config + w http.ResponseWriter + r *http.Request + Dir string + File string +} + +var routes = []route{ + {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack}, + {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack}, + {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs}, + {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks}, + {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile}, +} + +// Request handling function +func HttpBackend(config *Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + //log.Printf("%s %s %s %s", r.RemoteAddr, r.Method, r.URL.Path, r.Proto) + for _, route := range routes { + if m := route.cr.FindStringSubmatch(r.URL.Path); m != nil { + if route.method != r.Method { + renderMethodNotAllowed(w, r) + return + } + + file := strings.Replace(r.URL.Path, m[1]+"/", "", 1) + dir, err := getGitDir(config, m[1]) + + if err != nil { + log.Print(err) + renderNotFound(w) + return + } + + hr := handler{config, w, r, dir, file} + route.handler(hr) + return + } + } + renderNotFound(w) + return + } +} + +// Actual command handling functions + +func serviceUploadPack(hr handler) { + serviceRpc("upload-pack", hr) +} + +func serviceReceivePack(hr handler) { + serviceRpc("receive-pack", hr) +} + +func serviceRpc(rpc string, hr handler) { + w, r, dir := hr.w, hr.r, hr.Dir + access := hasAccess(r, hr.Config, dir, rpc, true) + + if access == false { + renderNoAccess(w) + return + } + + input, _ := ioutil.ReadAll(r.Body) + + w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc)) + w.WriteHeader(http.StatusOK) + + args := []string{rpc, "--stateless-rpc", dir} + cmd := exec.Command(hr.Config.GitBinPath, args...) + cmd.Dir = dir + in, err := cmd.StdinPipe() + if err != nil { + log.Print(err) + return + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Print(err) + return + } + + err = cmd.Start() + if err != nil { + log.Print(err) + return + } + + in.Write(input) + io.Copy(w, stdout) + cmd.Wait() + + if hr.Config.OnSucceed != nil { + hr.Config.OnSucceed(rpc, input) + } +} + +func getInfoRefs(hr handler) { + w, r, dir := hr.w, hr.r, hr.Dir + serviceName := getServiceType(r) + access := hasAccess(r, hr.Config, dir, serviceName, false) + + if access { + args := []string{serviceName, "--stateless-rpc", "--advertise-refs", "."} + refs := gitCommand(hr.Config.GitBinPath, dir, args...) + + hdrNocache(w) + w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", serviceName)) + w.WriteHeader(http.StatusOK) + w.Write(packetWrite("# service=git-" + serviceName + "\n")) + w.Write(packetFlush()) + w.Write(refs) + } else { + updateServerInfo(hr.Config.GitBinPath, dir) + hdrNocache(w) + sendFile("text/plain; charset=utf-8", hr) + } +} + +func getInfoPacks(hr handler) { + hdrCacheForever(hr.w) + sendFile("text/plain; charset=utf-8", hr) +} + +func getLooseObject(hr handler) { + hdrCacheForever(hr.w) + sendFile("application/x-git-loose-object", hr) +} + +func getPackFile(hr handler) { + hdrCacheForever(hr.w) + sendFile("application/x-git-packed-objects", hr) +} + +func getIdxFile(hr handler) { + hdrCacheForever(hr.w) + sendFile("application/x-git-packed-objects-toc", hr) +} + +func getTextFile(hr handler) { + hdrNocache(hr.w) + sendFile("text/plain", hr) +} + +// Logic helping functions + +func sendFile(contentType string, hr handler) { + w, r := hr.w, hr.r + reqFile := path.Join(hr.Dir, hr.File) + + //fmt.Println("sendFile:", reqFile) + + f, err := os.Stat(reqFile) + if os.IsNotExist(err) { + renderNotFound(w) + return + } + + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size())) + w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat)) + http.ServeFile(w, r, reqFile) +} + +func getGitDir(config *Config, filePath string) (string, error) { + root := config.ReposRoot + + if root == "" { + cwd, err := os.Getwd() + + if err != nil { + log.Print(err) + return "", err + } + + root = cwd + } + + f := path.Join(root, filePath) + if _, err := os.Stat(f); os.IsNotExist(err) { + return "", err + } + + return f, nil +} + +func getServiceType(r *http.Request) string { + serviceType := r.FormValue("service") + + if s := strings.HasPrefix(serviceType, "git-"); !s { + return "" + } + + return strings.Replace(serviceType, "git-", "", 1) +} + +func hasAccess(r *http.Request, config *Config, dir string, rpc string, checkContentType bool) bool { + if checkContentType { + if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) { + return false + } + } + + if !(rpc == "upload-pack" || rpc == "receive-pack") { + return false + } + if rpc == "receive-pack" { + return config.ReceivePack + } + if rpc == "upload-pack" { + return config.UploadPack + } + + return getConfigSetting(config.GitBinPath, rpc, dir) +} + +func getConfigSetting(gitBinPath, serviceName string, dir string) bool { + serviceName = strings.Replace(serviceName, "-", "", -1) + setting := getGitConfig(gitBinPath, "http."+serviceName, dir) + + if serviceName == "uploadpack" { + return setting != "false" + } + + return setting == "true" +} + +func getGitConfig(gitBinPath, configName string, dir string) string { + args := []string{"config", configName} + out := string(gitCommand(gitBinPath, dir, args...)) + return out[0 : len(out)-1] +} + +func updateServerInfo(gitBinPath, dir string) []byte { + args := []string{"update-server-info"} + return gitCommand(gitBinPath, dir, args...) +} + +func gitCommand(gitBinPath, dir string, args ...string) []byte { + command := exec.Command(gitBinPath, args...) + command.Dir = dir + out, err := command.Output() + + if err != nil { + log.Print(err) + } + + return out +} + +// HTTP error response handling functions + +func renderMethodNotAllowed(w http.ResponseWriter, r *http.Request) { + if r.Proto == "HTTP/1.1" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Method Not Allowed")) + } else { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Bad Request")) + } +} + +func renderNotFound(w http.ResponseWriter) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Not Found")) +} + +func renderNoAccess(w http.ResponseWriter) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("Forbidden")) +} + +// Packet-line handling function + +func packetFlush() []byte { + return []byte("0000") +} + +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) +} + +// Header writing functions + +func hdrNocache(w http.ResponseWriter) { + w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") +} + +func hdrCacheForever(w http.ResponseWriter) { + now := time.Now().Unix() + expires := now + 31536000 + w.Header().Set("Date", fmt.Sprintf("%d", now)) + w.Header().Set("Expires", fmt.Sprintf("%d", expires)) + w.Header().Set("Cache-Control", "public, max-age=31536000") +} + +// Main +/* +func main() { + http.HandleFunc("/", requestHandler()) + + err := http.ListenAndServe(":8080", nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +}*/ diff --git a/routers/repo/issue.go b/routers/repo/issue.go new file mode 100644 index 0000000000..9688fd4d94 --- /dev/null +++ b/routers/repo/issue.go @@ -0,0 +1,296 @@ +// 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 ( + "fmt" + "net/url" + "strings" + + "github.com/Unknwon/com" + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/middleware" +) + +func Issues(ctx *middleware.Context) { + ctx.Data["Title"] = "Issues" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = true + ctx.Data["ViewType"] = "all" + + milestoneId, _ := base.StrTo(ctx.Query("milestone")).Int() + page, _ := base.StrTo(ctx.Query("page")).Int() + + ctx.Data["IssueCreatedCount"] = 0 + + var posterId int64 = 0 + isCreatedBy := ctx.Query("type") == "created_by" + if isCreatedBy { + if !ctx.IsSigned { + ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) + ctx.Redirect("/user/login/", 302) + return + } + ctx.Data["ViewType"] = "created_by" + } + + // Get issues. + issues, err := models.GetIssues(0, ctx.Repo.Repository.Id, posterId, int64(milestoneId), page, + ctx.Query("state") == "closed", false, ctx.Query("labels"), ctx.Query("sortType")) + if err != nil { + ctx.Handle(200, "issue.Issues: %v", err) + return + } + + if ctx.IsSigned { + posterId = ctx.User.Id + } + var createdByCount int + + showIssues := make([]models.Issue, 0, len(issues)) + // Get posters. + for i := range issues { + u, err := models.GetUserById(issues[i].PosterId) + if err != nil { + ctx.Handle(200, "issue.Issues(get poster): %v", err) + return + } + if isCreatedBy && u.Id != posterId { + continue + } + if u.Id == posterId { + createdByCount++ + } + issues[i].Poster = u + showIssues = append(showIssues, issues[i]) + } + + ctx.Data["Issues"] = showIssues + ctx.Data["IssueCount"] = ctx.Repo.Repository.NumIssues + ctx.Data["OpenCount"] = ctx.Repo.Repository.NumOpenIssues + ctx.Data["ClosedCount"] = ctx.Repo.Repository.NumClosedIssues + ctx.Data["IssueCreatedCount"] = createdByCount + ctx.Data["IsShowClosed"] = ctx.Query("state") == "closed" + ctx.HTML(200, "issue/list") +} + +func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { + ctx.Data["Title"] = "Create issue" + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = false + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "issue/create") + return + } + + if ctx.HasError() { + ctx.HTML(200, "issue/create") + return + } + + issue, err := models.CreateIssue(ctx.User.Id, ctx.Repo.Repository.Id, form.MilestoneId, form.AssigneeId, + ctx.Repo.Repository.NumIssues, form.IssueName, form.Labels, form.Content, false) + if err != nil { + ctx.Handle(200, "issue.CreateIssue(CreateIssue)", err) + return + } + + // Notify watchers. + if err = models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email, + OpType: models.OP_CREATE_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), + RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil { + ctx.Handle(200, "issue.CreateIssue(NotifyWatchers)", err) + return + } + + // Mail watchers and mentions. + if base.Service.NotifyMail { + tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) + if err != nil { + ctx.Handle(200, "issue.CreateIssue(SendIssueNotifyMail)", err) + return + } + + tos = append(tos, ctx.User.LowerName) + ms := base.MentionPattern.FindAllString(issue.Content, -1) + newTos := make([]string, 0, len(ms)) + for _, m := range ms { + if com.IsSliceContainsStr(tos, m[1:]) { + continue + } + + newTos = append(newTos, m[1:]) + } + if err = mailer.SendIssueMentionMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, + issue, models.GetUserEmailsByNames(newTos)); err != nil { + ctx.Handle(200, "issue.CreateIssue(SendIssueMentionMail)", err) + return + } + } + + log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id) + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index)) +} + +func ViewIssue(ctx *middleware.Context, params martini.Params) { + index, err := base.StrTo(params["index"]).Int() + if err != nil { + ctx.Handle(404, "issue.ViewIssue", err) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(index)) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.ViewIssue", err) + } else { + ctx.Handle(200, "issue.ViewIssue", err) + } + return + } + + // Get posters. + u, err := models.GetUserById(issue.PosterId) + if err != nil { + ctx.Handle(200, "issue.ViewIssue(get poster): %v", err) + return + } + issue.Poster = u + issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)) + + // Get comments. + comments, err := models.GetIssueComments(issue.Id) + if err != nil { + ctx.Handle(200, "issue.ViewIssue(get comments): %v", err) + return + } + + // Get posters. + for i := range comments { + u, err := models.GetUserById(comments[i].PosterId) + if err != nil { + ctx.Handle(200, "issue.ViewIssue(get poster): %v", err) + return + } + comments[i].Poster = u + comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) + } + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue + ctx.Data["Comments"] = comments + ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id) + ctx.Data["IsRepoToolbarIssues"] = true + ctx.Data["IsRepoToolbarIssuesList"] = false + ctx.HTML(200, "issue/view") +} + +func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { + index, err := base.StrTo(params["index"]).Int() + if err != nil { + ctx.Handle(404, "issue.UpdateIssue", err) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(index)) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.UpdateIssue", err) + } else { + ctx.Handle(200, "issue.UpdateIssue(get issue)", err) + } + return + } + + if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner { + ctx.Handle(404, "issue.UpdateIssue", nil) + return + } + + issue.Name = form.IssueName + issue.MilestoneId = form.MilestoneId + issue.AssigneeId = form.AssigneeId + issue.Labels = form.Labels + issue.Content = form.Content + if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(200, "issue.UpdateIssue(update issue)", err) + return + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + "title": issue.Name, + "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)), + }) +} + +func Comment(ctx *middleware.Context, params martini.Params) { + index, err := base.StrTo(ctx.Query("issueIndex")).Int64() + if err != nil { + ctx.Handle(404, "issue.Comment(get index)", err) + return + } + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Handle(404, "issue.Comment", err) + } else { + ctx.Handle(200, "issue.Comment(get issue)", err) + } + return + } + + // Check if issue owner changes the status of issue. + var newStatus string + if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id { + newStatus = ctx.Query("change_status") + } + if len(newStatus) > 0 { + if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) || + (strings.Contains(newStatus, "Close") && !issue.IsClosed) { + issue.IsClosed = !issue.IsClosed + if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(200, "issue.Comment(update issue status)", err) + return + } + + cmtType := models.IT_CLOSE + if !issue.IsClosed { + cmtType = models.IT_REOPEN + } + + if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil { + ctx.Handle(200, "issue.Comment(create status change comment)", err) + return + } + log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed) + } + } + + content := ctx.Query("content") + if len(content) > 0 { + switch params["action"] { + case "new": + if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil { + ctx.Handle(500, "issue.Comment(create comment)", err) + return + } + log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id) + default: + ctx.Handle(404, "issue.Comment", err) + return + } + } + + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", ctx.User.Name, ctx.Repo.Repository.Name, index)) +} diff --git a/routers/repo/pull.go b/routers/repo/pull.go new file mode 100644 index 0000000000..430c6a815f --- /dev/null +++ b/routers/repo/pull.go @@ -0,0 +1,16 @@ +// 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 ( + "github.com/go-martini/martini" + + "github.com/gogits/gogs/modules/middleware" +) + +func Pulls(ctx *middleware.Context, params martini.Params) { + ctx.Data["IsRepoToolbarPulls"] = true + ctx.HTML(200, "repo/pulls") +} diff --git a/routers/repo/release.go b/routers/repo/release.go new file mode 100644 index 0000000000..279fc169f8 --- /dev/null +++ b/routers/repo/release.go @@ -0,0 +1,30 @@ +// 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 ( + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/middleware" +) + +func Releases(ctx *middleware.Context) { + ctx.Data["Title"] = "Releases" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.Data["IsRepoReleaseNew"] = false + tags, err := models.GetTags(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) + if err != nil { + ctx.Handle(404, "repo.Releases(GetTags)", err) + return + } + ctx.Data["Releases"] = tags + ctx.HTML(200, "release/list") +} + +func ReleasesNew(ctx *middleware.Context) { + ctx.Data["Title"] = "New Release" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.Data["IsRepoReleaseNew"] = true + ctx.HTML(200, "release/new") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go new file mode 100644 index 0000000000..d4d52ba0d7 --- /dev/null +++ b/routers/repo/repo.go @@ -0,0 +1,426 @@ +// 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 ( + "encoding/base64" + "errors" + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" +) + +func Create(ctx *middleware.Context, form auth.CreateRepoForm) { + ctx.Data["Title"] = "Create repository" + ctx.Data["PageIsNewRepo"] = true // For navbar arrow. + ctx.Data["LanguageIgns"] = models.LanguageIgns + ctx.Data["Licenses"] = models.Licenses + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "repo/create") + return + } + + if ctx.HasError() { + ctx.HTML(200, "repo/create") + return + } + + _, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, + form.Language, form.License, form.Visibility == "private", form.InitReadme == "on") + if err == nil { + log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) + ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) + return + } else if err == models.ErrRepoAlreadyExist { + ctx.RenderWithErr("Repository name has already been used", "repo/create", &form) + return + } else if err == models.ErrRepoNameIllegal { + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/create", &form) + return + } + ctx.Handle(200, "repo.Create", err) +} + +func Mirror(ctx *middleware.Context, form auth.CreateRepoForm) { + ctx.Data["Title"] = "Mirror repository" + ctx.Data["PageIsNewRepo"] = true // For navbar arrow. + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "repo/mirror") + return + } + + if ctx.HasError() { + ctx.HTML(200, "repo/mirror") + return + } + + _, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, + "", form.License, form.Visibility == "private", false) + if err == nil { + log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) + ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) + return + } else if err == models.ErrRepoAlreadyExist { + ctx.RenderWithErr("Repository name has already been used", "repo/mirror", &form) + return + } else if err == models.ErrRepoNameIllegal { + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/mirror", &form) + return + } + ctx.Handle(200, "repo.Mirror", err) +} + +func Single(ctx *middleware.Context, params martini.Params) { + branchName := ctx.Repo.BranchName + commitId := ctx.Repo.CommitId + userName := ctx.Repo.Owner.Name + repoName := ctx.Repo.Repository.Name + + repoLink := ctx.Repo.RepoLink + branchLink := ctx.Repo.RepoLink + "/src/" + branchName + rawLink := ctx.Repo.RepoLink + "/raw/" + branchName + + // Get tree path + treename := params["_1"] + + if len(treename) > 0 && treename[len(treename)-1] == '/' { + ctx.Redirect(repoLink + "/src/" + branchName + "/" + treename[:len(treename)-1]) + return + } + + ctx.Data["IsRepoToolbarSource"] = true + + // Branches. + brs, err := models.GetBranches(userName, repoName) + if err != nil { + ctx.Handle(404, "repo.Single(GetBranches)", err) + return + } + + ctx.Data["Branches"] = brs + + isViewBranch := ctx.Repo.IsBranch + ctx.Data["IsViewBranch"] = isViewBranch + + repoFile, err := models.GetTargetFile(userName, repoName, + branchName, commitId, treename) + + if err != nil && err != models.ErrRepoFileNotExist { + ctx.Handle(404, "repo.Single(GetTargetFile)", err) + return + } + + if len(treename) != 0 && repoFile == nil { + ctx.Handle(404, "repo.Single", nil) + return + } + + if repoFile != nil && repoFile.IsFile() { + if blob, err := repoFile.LookupBlob(); err != nil { + ctx.Handle(404, "repo.Single(repoFile.LookupBlob)", err) + } else { + ctx.Data["FileSize"] = repoFile.Size + ctx.Data["IsFile"] = true + ctx.Data["FileName"] = repoFile.Name + ext := path.Ext(repoFile.Name) + if len(ext) > 0 { + ext = ext[1:] + } + ctx.Data["FileExt"] = ext + ctx.Data["FileLink"] = rawLink + "/" + treename + + data := blob.Contents() + _, isTextFile := base.IsTextFile(data) + _, isImageFile := base.IsImageFile(data) + ctx.Data["FileIsText"] = isTextFile + + if isImageFile { + ctx.Data["IsImageFile"] = true + } else { + readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) + ctx.Data["ReadmeExist"] = readmeExist + if readmeExist { + ctx.Data["FileContent"] = string(base.RenderMarkdown(data, "")) + } else { + if isTextFile { + ctx.Data["FileContent"] = string(data) + } + } + } + } + + } else { + // Directory and file list. + files, err := models.GetReposFiles(userName, repoName, ctx.Repo.CommitId, treename) + if err != nil { + ctx.Handle(404, "repo.Single(GetReposFiles)", err) + return + } + + ctx.Data["Files"] = files + + var readmeFile *models.RepoFile + + for _, f := range files { + if !f.IsFile() || !base.IsReadmeFile(f.Name) { + continue + } else { + readmeFile = f + break + } + } + + if readmeFile != nil { + ctx.Data["ReadmeInSingle"] = true + ctx.Data["ReadmeExist"] = true + if blob, err := readmeFile.LookupBlob(); err != nil { + ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err) + return + } else { + ctx.Data["FileSize"] = readmeFile.Size + ctx.Data["FileLink"] = rawLink + "/" + treename + data := blob.Contents() + _, isTextFile := base.IsTextFile(data) + ctx.Data["FileIsText"] = isTextFile + ctx.Data["FileName"] = readmeFile.Name + if isTextFile { + ctx.Data["FileContent"] = string(base.RenderMarkdown(data, branchLink)) + } + } + } + } + + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + + var treenames []string + Paths := make([]string, 0) + + if len(treename) > 0 { + treenames = strings.Split(treename, "/") + for i, _ := range treenames { + Paths = append(Paths, strings.Join(treenames[0:i+1], "/")) + } + + ctx.Data["HasParentPath"] = true + if len(Paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + Paths[len(Paths)-2] + } + } + + ctx.Data["LastCommit"] = ctx.Repo.Commit + ctx.Data["Paths"] = Paths + ctx.Data["Treenames"] = treenames + ctx.Data["BranchLink"] = branchLink + ctx.HTML(200, "repo/single") +} + +func SingleDownload(ctx *middleware.Context, params martini.Params) { + // Get tree path + treename := params["_1"] + + branchName := params["branchname"] + userName := params["username"] + repoName := params["reponame"] + + var commitId string + if !models.IsBranchExist(userName, repoName, branchName) { + commitId = branchName + branchName = "" + } + + repoFile, err := models.GetTargetFile(userName, repoName, + branchName, commitId, treename) + + if err != nil { + ctx.Handle(404, "repo.SingleDownload(GetTargetFile)", err) + return + } + + blob, err := repoFile.LookupBlob() + if err != nil { + ctx.Handle(404, "repo.SingleDownload(LookupBlob)", err) + return + } + + data := blob.Contents() + contentType, isTextFile := base.IsTextFile(data) + _, isImageFile := base.IsImageFile(data) + ctx.Res.Header().Set("Content-Type", contentType) + if !isTextFile && !isImageFile { + ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename)) + ctx.Res.Header().Set("Content-Transfer-Encoding", "binary") + } + ctx.Res.Write(data) +} + +func basicEncode(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} + +func basicDecode(encoded string) (user string, name string, err error) { + var s []byte + s, err = base64.StdEncoding.DecodeString(encoded) + if err != nil { + return + } + + a := strings.Split(string(s), ":") + if len(a) == 2 { + user, name = a[0], a[1] + } else { + err = errors.New("decode failed") + } + return +} + +func authRequired(ctx *middleware.Context) { + ctx.ResponseWriter.Header().Set("WWW-Authenticate", "Basic realm=\".\"") + ctx.Data["ErrorMsg"] = "no basic auth and digit auth" + ctx.HTML(401, fmt.Sprintf("status/401")) +} + +func Setting(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "repo.Setting", nil) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + + var title string + if t, ok := ctx.Data["Title"].(string); ok { + title = t + } + + ctx.Data["Title"] = title + " - settings" + ctx.HTML(200, "repo/setting") +} + +func SettingPost(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + switch ctx.Query("action") { + case "update": + isNameChanged := false + newRepoName := ctx.Query("name") + // Check if repository name has been changed. + if ctx.Repo.Repository.Name != newRepoName { + isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) + if err != nil { + ctx.Handle(404, "repo.SettingPost(update: check existence)", err) + return + } else if isExist { + ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil) + return + } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { + ctx.Handle(404, "repo.SettingPost(change repository name)", err) + return + } + log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) + + isNameChanged = true + ctx.Repo.Repository.Name = newRepoName + } + + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + ctx.Repo.Repository.IsGoget = ctx.Query("goget") == "on" + if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { + ctx.Handle(404, "repo.SettingPost(update)", err) + return + } + + ctx.Data["IsSuccess"] = true + if isNameChanged { + ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) + } else { + ctx.HTML(200, "repo/setting") + } + log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) + case "transfer": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + return + } + + newOwner := ctx.Query("owner") + // Check if new owner exists. + isExist, err := models.IsUserExist(newOwner) + if err != nil { + ctx.Handle(404, "repo.SettingPost(transfer: check existence)", err) + return + } else if !isExist { + ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil) + return + } else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil { + ctx.Handle(404, "repo.SettingPost(transfer repository)", err) + return + } + log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) + + ctx.Redirect("/") + return + case "delete": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + return + } + + if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { + ctx.Handle(200, "repo.Delete", err) + return + } + + log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) + ctx.Redirect("/") + } +} + +func Action(ctx *middleware.Context, params martini.Params) { + var err error + switch 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 "desc": + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + err = models.UpdateRepository(ctx.Repo.Repository) + } + + if err != nil { + log.Error("repo.Action(%s): %v", params["action"], err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} diff --git a/routers/user/setting.go b/routers/user/setting.go new file mode 100644 index 0000000000..ea779e8549 --- /dev/null +++ b/routers/user/setting.go @@ -0,0 +1,184 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "strconv" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" +) + +// Render user setting page (email, website modify) +func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) { + ctx.Data["Title"] = "Setting" + ctx.Data["PageIsUserSetting"] = true // For navbar arrow. + ctx.Data["IsUserPageSetting"] = true // For setting nav highlight. + + user := ctx.User + ctx.Data["Owner"] = user + + if ctx.Req.Method == "GET" || ctx.HasError() { + ctx.HTML(200, "user/setting") + return + } + + // Check if user name has been changed. + if user.Name != form.UserName { + isExist, err := models.IsUserExist(form.UserName) + if err != nil { + ctx.Handle(404, "user.Setting(update: check existence)", err) + return + } else if isExist { + ctx.RenderWithErr("User name has been taken.", "user/setting", &form) + return + } else if err = models.ChangeUserName(user, form.UserName); err != nil { + ctx.Handle(404, "user.Setting(change user name)", err) + return + } + log.Trace("%s User name changed: %s -> %s", ctx.Req.RequestURI, user.Name, form.UserName) + + user.Name = form.UserName + } + + user.Email = form.Email + user.Website = form.Website + user.Location = form.Location + user.Avatar = base.EncodeMd5(form.Avatar) + user.AvatarEmail = form.Avatar + if err := models.UpdateUser(user); err != nil { + ctx.Handle(200, "setting.Setting", err) + return + } + + ctx.Data["IsSuccess"] = true + ctx.HTML(200, "user/setting") + log.Trace("%s User setting updated: %s", ctx.Req.RequestURI, ctx.User.LowerName) +} + +func SettingPassword(ctx *middleware.Context, form auth.UpdatePasswdForm) { + ctx.Data["Title"] = "Password" + ctx.Data["PageIsUserSetting"] = true + ctx.Data["IsUserPageSettingPasswd"] = true + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "user/password") + return + } + + user := ctx.User + newUser := &models.User{Passwd: form.NewPasswd} + newUser.EncodePasswd() + if user.Passwd != newUser.Passwd { + ctx.Data["HasError"] = true + ctx.Data["ErrorMsg"] = "Old password is not correct" + } else if form.NewPasswd != form.RetypePasswd { + ctx.Data["HasError"] = true + ctx.Data["ErrorMsg"] = "New password and re-type password are not same" + } else { + newUser.Salt = models.GetUserSalt() + user.Passwd = newUser.Passwd + if err := models.UpdateUser(user); err != nil { + ctx.Handle(200, "setting.SettingPassword", err) + return + } + ctx.Data["IsSuccess"] = true + } + + ctx.Data["Owner"] = user + ctx.HTML(200, "user/password") + log.Trace("%s User password updated: %s", ctx.Req.RequestURI, ctx.User.LowerName) +} + +func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) { + ctx.Data["Title"] = "SSH Keys" + + // Delete SSH key. + if ctx.Req.Method == "DELETE" || ctx.Query("_method") == "DELETE" { + id, err := strconv.ParseInt(ctx.Query("id"), 10, 64) + if err != nil { + log.Error("ssh.DelPublicKey: %v", err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + k := &models.PublicKey{ + Id: id, + OwnerId: ctx.User.Id, + } + + if err = models.DeletePublicKey(k); err != nil { + log.Error("ssh.DelPublicKey: %v", err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + } else { + log.Trace("%s User SSH key deleted: %s", ctx.Req.RequestURI, ctx.User.LowerName) + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) + } + return + } + + // Add new SSH key. + if ctx.Req.Method == "POST" { + if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) { + ctx.HTML(200, "user/publickey") + return + } + + k := &models.PublicKey{OwnerId: ctx.User.Id, + Name: form.KeyName, + Content: form.KeyContent, + } + + if err := models.AddPublicKey(k); err != nil { + if err.Error() == models.ErrKeyAlreadyExist.Error() { + ctx.RenderWithErr("Public key name has been used", "user/publickey", &form) + return + } + ctx.Handle(200, "ssh.AddPublicKey", err) + log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName) + return + } else { + ctx.Data["AddSSHKeySuccess"] = true + } + } + + // List existed SSH keys. + keys, err := models.ListPublicKey(ctx.User.Id) + if err != nil { + ctx.Handle(200, "ssh.ListPublicKey", err) + return + } + + ctx.Data["PageIsUserSetting"] = true + ctx.Data["IsUserPageSettingSSH"] = true + ctx.Data["Keys"] = keys + ctx.HTML(200, "user/publickey") +} + +func SettingNotification(ctx *middleware.Context) { + // TODO: user setting notification + ctx.Data["Title"] = "Notification" + ctx.Data["PageIsUserSetting"] = true + ctx.Data["IsUserPageSettingNotify"] = true + ctx.HTML(200, "user/notification") +} + +func SettingSecurity(ctx *middleware.Context) { + // TODO: user setting security + ctx.Data["Title"] = "Security" + ctx.Data["PageIsUserSetting"] = true + ctx.Data["IsUserPageSettingSecurity"] = true + ctx.HTML(200, "user/security") +} diff --git a/routers/user/social.go b/routers/user/social.go new file mode 100644 index 0000000000..b87c313f5d --- /dev/null +++ b/routers/user/social.go @@ -0,0 +1,159 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "encoding/json" + "net/http" + "net/url" + "strconv" + "strings" + + "code.google.com/p/goauth2/oauth" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/oauth2" +) + +type SocialConnector interface { + Identity() string + Type() int + Name() string + Email() string + Token() string +} + +type SocialGithub struct { + data struct { + Id int `json:"id"` + Name string `json:"login"` + Email string `json:"email"` + } + WebToken *oauth.Token +} + +func (s *SocialGithub) Identity() string { + return strconv.Itoa(s.data.Id) +} + +func (s *SocialGithub) Type() int { + return models.OT_GITHUB +} + +func (s *SocialGithub) Name() string { + return s.data.Name +} + +func (s *SocialGithub) Email() string { + return s.data.Email +} + +func (s *SocialGithub) Token() string { + data, _ := json.Marshal(s.WebToken) + return string(data) +} + +// Github API refer: https://developer.github.com/v3/users/ +func (s *SocialGithub) Update() error { + scope := "https://api.github.com/user" + transport := &oauth.Transport{ + Token: s.WebToken, + } + log.Debug("update github info") + r, err := transport.Client().Get(scope) + if err != nil { + return err + } + defer r.Body.Close() + return json.NewDecoder(r.Body).Decode(&s.data) +} + +func extractPath(next string) string { + n, err := url.Parse(next) + if err != nil { + return "/" + } + return n.Path +} + +// github && google && ... +func SocialSignIn(ctx *middleware.Context, tokens oauth2.Tokens) { + var socid int64 + var ok bool + next := extractPath(ctx.Query("next")) + log.Debug("social signed check %s", next) + if socid, ok = ctx.Session.Get("socialId").(int64); ok && socid != 0 { + // already login + ctx.Redirect(next) + log.Info("login soc id: %v", socid) + return + } + config := &oauth.Config{ + //ClientId: base.OauthService.Github.ClientId, + //ClientSecret: base.OauthService.Github.ClientSecret, // FIXME: I don't know why compile error here + ClientId: "09383403ff2dc16daaa1", + ClientSecret: "0e4aa0c3630df396cdcea01a9d45cacf79925fea", + RedirectURL: strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.RequestURI(), + Scope: base.OauthService.GitHub.Scopes, + AuthURL: "https://github.com/login/oauth/authorize", + TokenURL: "https://github.com/login/oauth/access_token", + } + transport := &oauth.Transport{ + Config: config, + Transport: http.DefaultTransport, + } + code := ctx.Query("code") + if code == "" { + // redirect to social login page + ctx.Redirect(config.AuthCodeURL(next)) + return + } + + // handle call back + tk, err := transport.Exchange(code) + if err != nil { + log.Error("oauth2 handle callback error: %v", err) + return // FIXME, need error page 501 + } + next = extractPath(ctx.Query("state")) + log.Debug("success token: %v", tk) + + gh := &SocialGithub{WebToken: tk} + if err = gh.Update(); err != nil { + // FIXME: handle error page 501 + log.Error("connect with github error: %s", err) + return + } + var soc SocialConnector = gh + log.Info("login: %s", soc.Name()) + oa, err := models.GetOauth2(soc.Identity()) + switch err { + case nil: + ctx.Session.Set("userId", oa.User.Id) + ctx.Session.Set("userName", oa.User.Name) + case models.ErrOauth2RecordNotExists: + oa = &models.Oauth2{} + oa.Uid = 0 + oa.Type = soc.Type() + oa.Token = soc.Token() + oa.Identity = soc.Identity() + log.Debug("oa: %v", oa) + if err = models.AddOauth2(oa); err != nil { + log.Error("add oauth2 %v", err) // 501 + return + } + case models.ErrOauth2NotAssociatedWithUser: + // ignore it. judge in /usr/login page + default: + log.Error(err.Error()) // FIXME: handle error page + return + } + ctx.Session.Set("socialId", oa.Id) + log.Debug("socialId: %v", oa.Id) + ctx.Redirect(next) +} diff --git a/routers/user/user.go b/routers/user/user.go new file mode 100644 index 0000000000..084d0bbde2 --- /dev/null +++ b/routers/user/user.go @@ -0,0 +1,514 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package user + +import ( + "fmt" + "net/url" + "strings" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/middleware" +) + +func Dashboard(ctx *middleware.Context) { + ctx.Data["Title"] = "Dashboard" + ctx.Data["PageIsUserDashboard"] = true + repos, err := models.GetRepositories(&models.User{Id: ctx.User.Id}) + if err != nil { + ctx.Handle(200, "user.Dashboard", err) + return + } + ctx.Data["MyRepos"] = repos + + feeds, err := models.GetFeeds(ctx.User.Id, 0, false) + if err != nil { + ctx.Handle(200, "user.Dashboard", err) + return + } + ctx.Data["Feeds"] = feeds + ctx.HTML(200, "user/dashboard") +} + +func Profile(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Profile" + + // TODO: Need to check view self or others. + user, err := models.GetUserByName(params["username"]) + if err != nil { + ctx.Handle(200, "user.Profile", err) + return + } + + ctx.Data["Owner"] = user + + tab := ctx.Query("tab") + ctx.Data["TabName"] = tab + + switch tab { + case "activity": + feeds, err := models.GetFeeds(user.Id, 0, true) + if err != nil { + ctx.Handle(200, "user.Profile", err) + return + } + ctx.Data["Feeds"] = feeds + default: + repos, err := models.GetRepositories(user) + if err != nil { + ctx.Handle(200, "user.Profile", err) + return + } + ctx.Data["Repos"] = repos + } + + ctx.Data["PageIsUserProfile"] = true + ctx.HTML(200, "user/profile") +} + +func SignIn(ctx *middleware.Context, form auth.LogInForm) { + ctx.Data["Title"] = "Log In" + + if ctx.Req.Method == "GET" { + if base.OauthService != nil { + ctx.Data["OauthEnabled"] = true + ctx.Data["OauthGitHubEnabled"] = base.OauthService.GitHub.Enabled + } + + // Check auto-login. + userName := ctx.GetCookie(base.CookieUserName) + if len(userName) == 0 { + ctx.HTML(200, "user/signin") + return + } + + isSucceed := false + defer func() { + if !isSucceed { + log.Trace("%s auto-login cookie cleared: %s", ctx.Req.RequestURI, userName) + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) + } + }() + + user, err := models.GetUserByName(userName) + if err != nil { + ctx.HTML(200, "user/signin") + return + } + + secret := base.EncodeMd5(user.Rands + user.Passwd) + value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName) + if value != user.Name { + ctx.HTML(200, "user/signin") + return + } + + isSucceed = true + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } + return + } + + if ctx.HasError() { + ctx.HTML(200, "user/signin") + return + } + + user, err := models.LoginUserPlain(form.UserName, form.Password) + if err != nil { + if err == models.ErrUserNotExist { + log.Trace("%s Log in failed: %s/%s", ctx.Req.RequestURI, form.UserName, form.Password) + ctx.RenderWithErr("Username or password is not correct", "user/signin", &form) + return + } + + ctx.Handle(200, "user.SignIn", err) + return + } + + if form.Remember == "on" { + secret := base.EncodeMd5(user.Rands + user.Passwd) + days := 86400 * base.LogInRememberDays + ctx.SetCookie(base.CookieUserName, user.Name, days) + ctx.SetSecureCookie(secret, base.CookieRememberName, user.Name, days) + } + + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } +} + +func SignOut(ctx *middleware.Context) { + ctx.Session.Delete("userId") + ctx.Session.Delete("userName") + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) + ctx.Redirect("/") +} + +func SignUp(ctx *middleware.Context, form auth.RegisterForm) { + ctx.Data["Title"] = "Sign Up" + ctx.Data["PageIsSignUp"] = true + + if base.Service.DisenableRegisteration { + ctx.Data["DisenableRegisteration"] = true + ctx.HTML(200, "user/signup") + return + } + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "user/signup") + return + } + + if form.Password != form.RetypePasswd { + ctx.Data["HasError"] = true + ctx.Data["Err_Password"] = true + ctx.Data["Err_RetypePasswd"] = true + ctx.Data["ErrorMsg"] = "Password and re-type password are not same" + auth.AssignForm(form, ctx.Data) + } + + if ctx.HasError() { + ctx.HTML(200, "user/signup") + return + } + + u := &models.User{ + Name: form.UserName, + Email: form.Email, + Passwd: form.Password, + IsActive: !base.Service.RegisterEmailConfirm, + } + + var err error + if u, err = models.RegisterUser(u); err != nil { + switch err { + case models.ErrUserAlreadyExist: + ctx.RenderWithErr("Username has been already taken", "user/signup", &form) + case models.ErrEmailAlreadyUsed: + ctx.RenderWithErr("E-mail address has been already used", "user/signup", &form) + case models.ErrUserNameIllegal: + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form) + default: + ctx.Handle(200, "user.SignUp", err) + } + return + } + + log.Trace("%s User created: %s", ctx.Req.RequestURI, strings.ToLower(form.UserName)) + + // Send confirmation e-mail. + if base.Service.RegisterEmailConfirm && u.Id > 1 { + mailer.SendRegisterMail(ctx.Render, u) + ctx.Data["IsSendRegisterMail"] = true + ctx.Data["Email"] = u.Email + ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60 + ctx.HTML(200, "user/active") + + if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error("Set cache(MailResendLimit) fail: %v", err) + } + return + } + ctx.Redirect("/user/login") +} + +func Delete(ctx *middleware.Context) { + ctx.Data["Title"] = "Delete Account" + ctx.Data["PageIsUserSetting"] = true + ctx.Data["IsUserPageSettingDelete"] = true + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "user/delete") + return + } + + tmpUser := models.User{Passwd: ctx.Query("password")} + tmpUser.EncodePasswd() + if len(tmpUser.Passwd) == 0 || tmpUser.Passwd != ctx.User.Passwd { + ctx.Data["HasError"] = true + ctx.Data["ErrorMsg"] = "Password is not correct. Make sure you are owner of this account." + } else { + if err := models.DeleteUser(ctx.User); err != nil { + ctx.Data["HasError"] = true + switch err { + case models.ErrUserOwnRepos: + ctx.Data["ErrorMsg"] = "Your account still have ownership of repository, you have to delete or transfer them first." + default: + ctx.Handle(200, "user.Delete", err) + return + } + } else { + ctx.Redirect("/") + return + } + } + + ctx.HTML(200, "user/delete") +} + +const ( + TPL_FEED = `<i class="icon fa fa-%s"></i> + <div class="info"><span class="meta">%s</span><br>%s</div>` +) + +func Feeds(ctx *middleware.Context, form auth.FeedsForm) { + actions, err := models.GetFeeds(form.UserId, form.Page*20, false) + if err != nil { + ctx.JSON(500, err) + } + + feeds := make([]string, len(actions)) + for i := range actions { + feeds[i] = fmt.Sprintf(TPL_FEED, base.ActionIcon(actions[i].OpType), + base.TimeSince(actions[i].Created), base.ActionDesc(actions[i])) + } + ctx.JSON(200, &feeds) +} + +func Issues(ctx *middleware.Context) { + ctx.Data["Title"] = "Your Issues" + ctx.Data["ViewType"] = "all" + + page, _ := base.StrTo(ctx.Query("page")).Int() + repoId, _ := base.StrTo(ctx.Query("repoid")).Int64() + + ctx.Data["RepoId"] = repoId + + var posterId int64 = 0 + if ctx.Query("type") == "created_by" { + posterId = ctx.User.Id + ctx.Data["ViewType"] = "created_by" + } + + // Get all repositories. + repos, err := models.GetRepositories(ctx.User) + if err != nil { + ctx.Handle(200, "user.Issues(get repositories)", err) + return + } + + showRepos := make([]models.Repository, 0, len(repos)) + + isShowClosed := ctx.Query("state") == "closed" + var closedIssueCount, createdByCount, allIssueCount int + + // Get all issues. + allIssues := make([]models.Issue, 0, 5*len(repos)) + for i, repo := range repos { + issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "") + if err != nil { + ctx.Handle(200, "user.Issues(get issues)", err) + return + } + + allIssueCount += repo.NumIssues + closedIssueCount += repo.NumClosedIssues + + // Set repository information to issues. + for j := range issues { + issues[j].Repo = &repos[i] + } + allIssues = append(allIssues, issues...) + + repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues + if repos[i].NumOpenIssues > 0 { + showRepos = append(showRepos, repos[i]) + } + } + + showIssues := make([]models.Issue, 0, len(allIssues)) + ctx.Data["IsShowClosed"] = isShowClosed + + // Get posters and filter issues. + for i := range allIssues { + u, err := models.GetUserById(allIssues[i].PosterId) + if err != nil { + ctx.Handle(200, "user.Issues(get poster): %v", err) + return + } + allIssues[i].Poster = u + if u.Id == ctx.User.Id { + createdByCount++ + } + + if repoId > 0 && repoId != allIssues[i].Repo.Id { + continue + } + + if isShowClosed == allIssues[i].IsClosed { + showIssues = append(showIssues, allIssues[i]) + } + } + + ctx.Data["Repos"] = showRepos + ctx.Data["Issues"] = showIssues + ctx.Data["AllIssueCount"] = allIssueCount + ctx.Data["ClosedIssueCount"] = closedIssueCount + ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount + ctx.Data["CreatedByCount"] = createdByCount + ctx.HTML(200, "issue/user") +} + +func Pulls(ctx *middleware.Context) { + ctx.HTML(200, "user/pulls") +} + +func Stars(ctx *middleware.Context) { + ctx.HTML(200, "user/stars") +} + +func Activate(ctx *middleware.Context) { + code := ctx.Query("code") + if len(code) == 0 { + ctx.Data["IsActivatePage"] = true + if ctx.User.IsActive { + ctx.Handle(404, "user.Activate", nil) + return + } + // Resend confirmation e-mail. + if base.Service.RegisterEmailConfirm { + if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) { + ctx.Data["ResendLimited"] = true + } else { + ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60 + mailer.SendActiveMail(ctx.Render, 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(200, "user/active") + return + } + + // Verify code. + if user := models.VerifyUserActiveCode(code); user != nil { + user.IsActive = true + user.Rands = models.GetUserSalt() + if err := models.UpdateUser(user); err != nil { + ctx.Handle(404, "user.Activate", err) + return + } + + log.Trace("%s User activated: %s", ctx.Req.RequestURI, user.Name) + + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + ctx.Redirect("/") + return + } + + ctx.Data["IsActivateFailed"] = true + ctx.HTML(200, "user/active") +} + +func ForgotPasswd(ctx *middleware.Context) { + ctx.Data["Title"] = "Forgot Password" + + if base.MailService == nil { + ctx.Data["IsResetDisable"] = true + ctx.HTML(200, "user/forgot_passwd") + return + } + + ctx.Data["IsResetRequest"] = true + if ctx.Req.Method == "GET" { + ctx.HTML(200, "user/forgot_passwd") + return + } + + email := ctx.Query("email") + u, err := models.GetUserByEmail(email) + if err != nil { + if err == models.ErrUserNotExist { + ctx.RenderWithErr("This e-mail address does not associate to any account.", "user/forgot_passwd", nil) + } else { + ctx.Handle(404, "user.ResetPasswd(check existence)", err) + } + return + } + + if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) { + ctx.Data["ResendLimited"] = true + ctx.HTML(200, "user/forgot_passwd") + return + } + + mailer.SendResetPasswdMail(ctx.Render, u) + if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error("Set cache(MailResendLimit) fail: %v", err) + } + + ctx.Data["Email"] = email + ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60 + ctx.Data["IsResetSent"] = true + ctx.HTML(200, "user/forgot_passwd") +} + +func ResetPasswd(ctx *middleware.Context) { + code := ctx.Query("code") + if len(code) == 0 { + ctx.Error(404) + return + } + ctx.Data["Code"] = code + + if ctx.Req.Method == "GET" { + ctx.Data["IsResetForm"] = true + ctx.HTML(200, "user/reset_passwd") + return + } + + if u := models.VerifyUserActiveCode(code); u != nil { + // Validate password length. + passwd := ctx.Query("passwd") + if len(passwd) < 6 || len(passwd) > 30 { + ctx.Data["IsResetForm"] = true + ctx.RenderWithErr("Password length should be in 6 and 30.", "user/reset_passwd", nil) + return + } + + u.Passwd = passwd + u.Rands = models.GetUserSalt() + u.Salt = models.GetUserSalt() + u.EncodePasswd() + if err := models.UpdateUser(u); err != nil { + ctx.Handle(404, "user.ResetPasswd(UpdateUser)", err) + return + } + + log.Trace("%s User password reset: %s", ctx.Req.RequestURI, u.Name) + ctx.Redirect("/user/login") + return + } + + ctx.Data["IsResetFailed"] = true + ctx.HTML(200, "user/reset_passwd") +} |