diff options
Diffstat (limited to 'routers')
-rw-r--r-- | routers/admin/admin.go | 169 | ||||
-rw-r--r-- | routers/admin/user.go | 144 | ||||
-rw-r--r-- | routers/dashboard.go | 40 | ||||
-rw-r--r-- | routers/dev/template.go | 25 | ||||
-rw-r--r-- | routers/repo/branch.go | 38 | ||||
-rw-r--r-- | routers/repo/commit.go | 41 | ||||
-rw-r--r-- | routers/repo/issue.go | 125 | ||||
-rw-r--r-- | routers/repo/pull.go | 21 | ||||
-rw-r--r-- | routers/repo/repo.go | 365 | ||||
-rw-r--r-- | routers/user/setting.go | 176 | ||||
-rw-r--r-- | routers/user/user.go | 338 |
11 files changed, 1482 insertions, 0 deletions
diff --git a/routers/admin/admin.go b/routers/admin/admin.go new file mode 100644 index 00000000..0b5e3d8e --- /dev/null +++ b/routers/admin/admin.go @@ -0,0 +1,169 @@ +// 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/codegangsta/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["EnableHttpsClone"] = base.EnableHttpsClone + 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 00000000..7f66c552 --- /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/codegangsta/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/dashboard.go b/routers/dashboard.go new file mode 100644 index 00000000..2c81cf23 --- /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 00000000..d2f77ac4 --- /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/codegangsta/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/repo/branch.go b/routers/repo/branch.go new file mode 100644 index 00000000..8c953f2e --- /dev/null +++ b/routers/repo/branch.go @@ -0,0 +1,38 @@ +// 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/codegangsta/martini" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/middleware" +) + +func Branches(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Branches", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Branches", nil) + return + } + + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + ctx.Data["Branchname"] = params["branchname"] + 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 00000000..e038998f --- /dev/null +++ b/routers/repo/commit.go @@ -0,0 +1,41 @@ +// 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/codegangsta/martini" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/middleware" +) + +func Commits(ctx *middleware.Context, params martini.Params) { + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Commits", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.Commits", nil) + return + } + + ctx.Data["IsRepoToolbarCommits"] = true + commits, err := models.GetCommits(params["username"], + params["reponame"], params["branchname"]) + if err != nil { + ctx.Handle(404, "repo.Commits", nil) + return + } + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.HTML(200, "repo/commits") +} + +func Diff(ctx *middleware.Context,params martini.Params){ + ctx.Data["Title"] = "commit-sha" + ctx.Data["IsRepoToolbarCommits"] = true + ctx.HTML(200,"repo/diff") +} diff --git a/routers/repo/issue.go b/routers/repo/issue.go new file mode 100644 index 00000000..e03f115e --- /dev/null +++ b/routers/repo/issue.go @@ -0,0 +1,125 @@ +// 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" + + "github.com/codegangsta/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 Issues(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Issues" + ctx.Data["IsRepoToolbarIssues"] = true + + milestoneId, _ := base.StrTo(params["milestone"]).Int() + page, _ := base.StrTo(params["page"]).Int() + + var err error + ctx.Data["Issues"], err = models.GetIssues(0, ctx.Repo.Repository.Id, 0, + int64(milestoneId), page, params["state"] == "closed", false, params["labels"], params["sortType"]) + if err != nil { + ctx.Handle(200, "issue.Issues: %v", err) + return + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + ctx.Data["Branchname"] = params["branchname"] + ctx.HTML(200, "repo/issues") +} + +func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "issue.CreateIssue", nil) + return + } + + ctx.Data["Title"] = "Create issue" + + 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, form.RepoId, form.MilestoneId, form.AssigneeId, + form.IssueName, form.Labels, form.Content, false) + if err == nil { + log.Trace("%s Issue created: %d", form.RepoId, issue.Id) + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index)) + return + } + ctx.Handle(200, "issue.CreateIssue", err) +} + +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 + } + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue + ctx.HTML(200, "issue/view") +} + +func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "issue.UpdateIssue", nil) + return + } + + 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", err) + } + 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", err) + return + } + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue +} diff --git a/routers/repo/pull.go b/routers/repo/pull.go new file mode 100644 index 00000000..16c60389 --- /dev/null +++ b/routers/repo/pull.go @@ -0,0 +1,21 @@ +// 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/codegangsta/martini" + + "github.com/gogits/gogs/modules/middleware" +) + +func Pulls(ctx *middleware.Context, params martini.Params) { + ctx.Data["IsRepoToolbarPulls"] = true + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + ctx.Data["Branchname"] = params["branchname"] + ctx.HTML(200, "repo/pulls") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go new file mode 100644 index 00000000..cd28d52c --- /dev/null +++ b/routers/repo/repo.go @@ -0,0 +1,365 @@ +// 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 ( + "path" + "path/filepath" + "strings" + + "github.com/codegangsta/martini" + + "github.com/gogits/webdav" + + "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 Single(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + // Get tree path + treename := params["_1"] + + if len(treename) > 0 && treename[len(treename)-1] == '/' { + ctx.Redirect("/" + ctx.Repo.Owner.LowerName + "/" + + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + "/" + treename[:len(treename)-1]) + return + } + + ctx.Data["IsRepoToolbarSource"] = true + + // Branches. + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + //log.Error("repo.Single(GetBranches): %v", err) + ctx.Handle(404, "repo.Single(GetBranches)", err) + return + } else if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/single") + return + } + + ctx.Data["Branches"] = brs + + repoFile, err := models.GetTargetFile(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + + if err != nil && err != models.ErrRepoFileNotExist { + //log.Error("repo.Single(GetTargetFile): %v", err) + ctx.Handle(404, "repo.Single(GetTargetFile)", err) + return + } + + branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + rawLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/raw/" + params["branchname"] + + 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) + ctx.Data["FileIsText"] = isTextFile + + 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(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + if err != nil { + //log.Error("repo.Single(GetReposFiles): %v", err) + 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"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["Branchname"] = params["branchname"] + + 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] + } + } + + // Get latest commit according username and repo name + commit, err := models.GetCommit(params["username"], params["reponame"], + params["branchname"], params["commitid"]) + if err != nil { + log.Error("repo.Single(GetCommit): %v", err) + ctx.Handle(404, "repo.Single(GetCommit)", err) + return + } + ctx.Data["LastCommit"] = 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) { + if !ctx.Repo.IsValid { + ctx.Handle(404, "repo.SingleDownload", nil) + return + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + // Get tree path + treename := params["_1"] + + repoFile, err := models.GetTargetFile(params["username"], params["reponame"], + params["branchname"], params["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) + ctx.Res.Header().Set("Content-Type", contentType) + if !isTextFile { + ctx.Res.Header().Set("Content-Type", contentType) + ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename)) + ctx.Res.Header().Set("Content-Transfer-Encoding", "binary") + } + ctx.Res.Write(data) +} + +func Http(ctx *middleware.Context, params martini.Params) { + /*if !ctx.Repo.IsValid { + return + }*/ + + // TODO: access check + + username := params["username"] + reponame := params["reponame"] + if strings.HasSuffix(reponame, ".git") { + reponame = reponame[:len(reponame)-4] + } + + prefix := path.Join("/", username, params["reponame"]) + server := &webdav.Server{ + Fs: webdav.Dir(models.RepoPath(username, reponame)), + TrimPrefix: prefix, + Listings: true, + } + + server.ServeHTTP(ctx.ResponseWriter, ctx.Req) +} + +func Setting(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "repo.Setting", nil) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + + if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/setting") + return + } + + var title string + if t, ok := ctx.Data["Title"].(string); ok { + title = t + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + ctx.Data["Branchname"] = params["branchname"] + 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": + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { + ctx.Handle(404, "repo.SettingPost(update)", err) + return + } + ctx.Data["IsSuccess"] = true + ctx.HTML(200, "repo/setting") + log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) + case "delete": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.Data["ErrorMsg"] = "Please make sure you entered repository name is correct." + ctx.HTML(200, "repo/setting") + 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 00000000..75adf2b8 --- /dev/null +++ b/routers/user/setting.go @@ -0,0 +1,176 @@ +// 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.HTML(200, "user/setting") + return + } + + // below is for POST requests + if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) { + ctx.HTML(200, "user/setting") + return + } + + 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} + if err := newUser.EncodePasswd(); err != nil { + ctx.Handle(200, "setting.SettingPassword", err) + return + } + + 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 { + 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/user.go b/routers/user/user.go new file mode 100644 index 00000000..a0321f18 --- /dev/null +++ b/routers/user/user.go @@ -0,0 +1,338 @@ +// 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/codegangsta/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" { + // 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 hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) { + 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.User.AvatarLink())) + } + ctx.JSON(200, &feeds) +} + +func Issues(ctx *middleware.Context) { + ctx.HTML(200, "user/issues") +} + +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) + } + } 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() + models.UpdateUser(user) + + log.Trace("%s User activated: %s", ctx.Req.RequestURI, user.LowerName) + + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + ctx.Redirect("/") + return + } + + ctx.Data["IsActivateFailed"] = true + ctx.HTML(200, "user/active") +} |