对不起,该页面出错了。请反馈给我们
+将在3秒后跳转到首页
+将在3秒后跳转到首页
`) + } else { + data["error"] = template.HTML(`链接无效或过期,请重新操作。忘记密码?`) + } + return render(ctx, contentTpl, data) + } + + data["valid"] = true + data["code"] = uuid + // 提交修改密码 + if passwd != "" && method == "POST" { + // 简单校验 + if len(passwd) < 6 || len(passwd) > 32 { + data["error"] = "密码长度必须在6到32个字符之间" + } else if passwd != ctx.FormValue("pass2") { + data["error"] = "两次密码输入不一致" + } else { + // 更新密码 + _, err := logic.DefaultUser.ResetPasswd(context.EchoContext(ctx), email, passwd) + if err != nil { + data["error"] = "对不起,服务器错误,请重试!" + } else { + data["success"] = template.HTML(`密码重置成功,将在3秒后跳转到登录页面
`) + } + } + } + return render(ctx, contentTpl, data) +} + +// Logout 注销 +func (AccountController) Logout(ctx echo.Context) error { + // 删除cookie信息 + session := GetCookieSession(ctx) + session.Options = &sessions.Options{Path: "/", MaxAge: -1} + session.Save(Request(ctx), ResponseWriter(ctx)) + // 重定向得到原页面 + return ctx.Redirect(http.StatusSeeOther, ctx.Request().Referer()) +} + +// Unbind 第三方账号解绑 +func (AccountController) Unbind(ctx echo.Context) error { + bindId := ctx.FormValue("bind_id") + me := ctx.Get("user").(*model.Me) + logic.DefaultThirdUser.UnBindUser(context.EchoContext(ctx), bindId, me) + + return ctx.Redirect(http.StatusSeeOther, "/account/edit#connection") +} diff --git a/internal/http/controller/admin/article.go b/internal/http/controller/admin/article.go new file mode 100644 index 00000000..ff9d5d44 --- /dev/null +++ b/internal/http/controller/admin/article.go @@ -0,0 +1,168 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ArticleController struct{} + +// 注册路由 +func (self ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/article/list", self.ArticleList) + g.POST("/crawl/article/query.html", self.ArticleQuery) + g.POST("/crawl/article/move", self.MoveToTopic) + g.Match([]string{"GET", "POST"}, "/crawl/article/new", self.CrawlArticle) + g.Match([]string{"GET", "POST"}, "/crawl/article/publish", self.Publish) + g.Match([]string{"GET", "POST"}, "/crawl/article/modify", self.Modify) +} + +// ArticleList 所有文章(分页) +func (ArticleController) ArticleList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), nil, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "article/list.html,article/query.html", data) +} + +// ArticleQuery +func (ArticleController) ArticleQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "domain", "title"}) + + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "article/query.html", data) +} + +// CrawlArticle +func (ArticleController) CrawlArticle(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + urls := strings.Split(ctx.FormValue("urls"), "\n") + + var ( + errMsg string + err error + ) + for _, url := range urls { + url = strings.TrimSpace(url) + + if strings.HasPrefix(url, "http") { + _, err = logic.DefaultArticle.ParseArticle(context.EchoContext(ctx), url, false) + } else { + isAll := false + websiteInfo := strings.Split(url, ":") + if len(websiteInfo) >= 2 { + isAll = goutils.MustBool(websiteInfo[1]) + } + err = logic.DefaultAutoCrawl.CrawlWebsite(strings.TrimSpace(websiteInfo[0]), isAll) + } + + if err != nil { + errMsg = err.Error() + } + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "article/new.html", data) +} + +// Publish +func (self ArticleController) Publish(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultArticle.PublishFromAdmin(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) + } + + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/publish.html", data) +} + +// Modify +func (self ArticleController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.ArticleList))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/modify.html", data) +} + +// MoveToTopic 放入 Topic 中 +func (self ArticleController) MoveToTopic(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + err := logic.DefaultArticle.MoveToTopic(context.EchoContext(ctx), ctx.QueryParam("id"), user) + + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) +} diff --git a/internal/http/controller/admin/authority.go b/internal/http/controller/admin/authority.go new file mode 100644 index 00000000..7118fe97 --- /dev/null +++ b/internal/http/controller/admin/authority.go @@ -0,0 +1,155 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type AuthorityController struct{} + +// 注册路由 +func (self AuthorityController) RegisterRoute(g *echo.Group) { + g.GET("/user/auth/list", self.AuthList) + g.POST("/user/auth/query.html", self.AuthQuery) +} + +// AuthList 所有权限(分页) +func (AuthorityController) AuthList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + total := len(logic.Authorities) + newLimit := limit + if total < limit { + newLimit = total + } + + data := map[string]interface{}{ + "datalist": logic.Authorities[(curPage - 1):newLimit], + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "authority/list.html,authority/query.html", data) +} + +func (AuthorityController) AuthQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + conds := parseConds(ctx, []string{"route", "name"}) + + authorities, total := logic.DefaultAuthority.FindAuthoritiesByPage(context.EchoContext(ctx), conds, curPage, limit) + + if authorities == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": authorities, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "authority/query.html", data) +} + +// func NewAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// if req.PostFormValue("submit") == "1" { +// user, _ := filter.CurrentUser(req) +// username := user["username"].(string) + +// errMsg, err := service.SaveAuthority(req.PostForm, username) +// if err != nil { +// data["ok"] = 0 +// data["error"] = errMsg +// } else { +// data["ok"] = 1 +// data["msg"] = "添加成功" +// } +// } else { +// menu1, menu2 := service.GetMenus() +// allmenu2, _ := json.Marshal(menu2) + +// // 设置内容模板 +// req.Form.Set(filter.CONTENT_TPL_KEY, "/template/admin/authority/new.html") +// data["allmenu1"] = menu1 +// data["allmenu2"] = string(allmenu2) +// } + +// filter.SetData(req, data) +// } + +// func ModifyAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// if req.PostFormValue("submit") == "1" { +// user, _ := filter.CurrentUser(req) +// username := user["username"].(string) + +// errMsg, err := service.SaveAuthority(req.PostForm, username) +// if err != nil { +// data["ok"] = 0 +// data["error"] = errMsg +// } else { +// data["ok"] = 1 +// data["msg"] = "修改成功" +// } +// } else { +// menu1, menu2 := service.GetMenus() +// allmenu2, _ := json.Marshal(menu2) + +// authority := service.FindAuthority(req.FormValue("aid")) + +// if authority == nil || authority.Aid == 0 { +// rw.WriteHeader(http.StatusInternalServerError) +// return +// } + +// // 设置内容模板 +// req.Form.Set(filter.CONTENT_TPL_KEY, "/template/admin/authority/modify.html") +// data["allmenu1"] = menu1 +// data["allmenu2"] = string(allmenu2) +// data["authority"] = authority +// } + +// filter.SetData(req, data) +// } + +// func DelAuthorityHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// aid := req.FormValue("aid") + +// if _, err := strconv.Atoi(aid); err != nil { +// data["ok"] = 0 +// data["error"] = "aid不是整型" + +// filter.SetData(req, data) +// return +// } + +// if err := service.DelAuthority(aid); err != nil { +// data["ok"] = 0 +// data["error"] = "删除失败!" +// } else { +// data["ok"] = 1 +// data["msg"] = "删除成功!" +// } + +// filter.SetData(req, data) +// } diff --git a/internal/http/controller/admin/base.go b/internal/http/controller/admin/base.go new file mode 100644 index 00000000..215d8328 --- /dev/null +++ b/internal/http/controller/admin/base.go @@ -0,0 +1,94 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +func parsePage(ctx echo.Context) (curPage, limit int) { + curPage = goutils.MustInt(ctx.FormValue("page"), 1) + limit = goutils.MustInt(ctx.FormValue("limit"), 20) + return +} + +func parseConds(ctx echo.Context, fields []string) map[string]string { + conds := make(map[string]string) + + for _, field := range fields { + if value := ctx.FormValue(field); value != "" { + conds[field] = value + } + } + + return conds +} + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +// render html 输出 +func render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return RenderAdmin(ctx, contentTpl, data) +} + +func renderQuery(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return RenderQuery(ctx, contentTpl, data) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "ok": 1, + "msg": "操作成功", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, code int, msg string) error { + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + result := map[string]interface{}{ + "ok": 0, + "error": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/admin/index.go b/internal/http/controller/admin/index.go new file mode 100644 index 00000000..9835979d --- /dev/null +++ b/internal/http/controller/admin/index.go @@ -0,0 +1,15 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + echo "github.com/labstack/echo/v4" +) + +func AdminIndex(ctx echo.Context) error { + return render(ctx, "index.html", nil) +} diff --git a/internal/http/controller/admin/metrics.go b/internal/http/controller/admin/metrics.go new file mode 100644 index 00000000..5f7d83be --- /dev/null +++ b/internal/http/controller/admin/metrics.go @@ -0,0 +1,64 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "expvar" + + "net/http" + "strconv" + "time" + + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +var ( + onlineStats = expvar.NewMap("online_stats") + loginUserNum expvar.Int + visitorUserNum expvar.Int +) + +type MetricsController struct{} + +// 注册路由 +func (self MetricsController) RegisterRoute(g *echo.Group) { + g.GET("/debug/vars", self.DebugExpvar) + g.GET("/user/is_online", self.IsOnline) +} + +func (self MetricsController) DebugExpvar(ctx echo.Context) error { + loginUserNum.Set(int64(logic.Book.LoginLen())) + visitorUserNum.Set(int64(logic.Book.Len())) + + onlineStats.Set("login_user_num", &loginUserNum) + onlineStats.Set("visitor_user_num", &visitorUserNum) + onlineStats.Set("uptime", expvar.Func(self.calculateUptime)) + onlineStats.Set("login_user_data", logic.Book.LoginUserData()) + + handler := expvar.Handler() + handler.ServeHTTP(ResponseWriter(ctx), Request(ctx)) + return nil +} + +func (self MetricsController) IsOnline(ctx echo.Context) error { + uid := goutils.MustInt(ctx.FormValue("uid")) + + onlineInfo := map[string]int{"online": logic.Book.Len()} + message := logic.NewMessage(logic.WsMsgOnline, onlineInfo) + logic.Book.PostMessage(uid, message) + + return ctx.HTML(http.StatusOK, strconv.FormatBool(logic.Book.UserIsOnline(uid))) +} + +func (self MetricsController) calculateUptime() interface{} { + return time.Since(global.App.LaunchTime).String() +} diff --git a/internal/http/controller/admin/node.go b/internal/http/controller/admin/node.go new file mode 100644 index 00000000..54883d76 --- /dev/null +++ b/internal/http/controller/admin/node.go @@ -0,0 +1,98 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type NodeController struct{} + +// 注册路由 +func (self NodeController) RegisterRoute(g *echo.Group) { + g.GET("/community/node/list", self.List) + g.Match([]string{"GET", "POST"}, "/community/node/modify", self.Modify) + g.POST("/community/node/modify_seq", self.ModifySeq) +} + +// List 所有主题节点 +func (NodeController) List(ctx echo.Context) error { + treeNodes := logic.DefaultNode.FindParallelTree(context.EchoContext(ctx)) + + nidMap := make(map[int]int) + keySlice := make([]int, len(treeNodes)) + + for i, node := range treeNodes { + nidMap[node.Nid] = i + 1 + + if node.Parent > 0 { + keySlice[i] = nidMap[node.Parent] + } else { + keySlice[i] = 0 + } + } + + data := map[string]interface{}{ + "nodes": treeNodes, + "key_slice": keySlice, + } + + return render(ctx, "topic/node.html", data) +} + +func (NodeController) Modify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultNode.Modify(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + global.TopicNodeChan <- struct{}{} + return success(ctx, nil) + } + + treeNodes := logic.DefaultNode.FindParallelTree(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "nodes": treeNodes, + } + + nid := goutils.MustInt(ctx.QueryParam("nid")) + parent := goutils.MustInt(ctx.QueryParam("parent")) + if nid == 0 && parent == 0 { + // 新增 + data["node"] = &model.TopicNode{ + ShowIndex: true, + } + } else if nid > 0 { + data["node"] = logic.DefaultNode.FindOne(nid) + } else if parent > 0 { + data["node"] = &model.TopicNode{ + ShowIndex: true, + } + } + data["parent"] = parent + + return render(ctx, "topic/node_modify.html", data) +} + +func (NodeController) ModifySeq(ctx echo.Context) error { + nid := goutils.MustInt(ctx.FormValue("nid")) + seq := goutils.MustInt(ctx.FormValue("seq")) + err := logic.DefaultNode.ModifySeq(context.EchoContext(ctx), nid, seq) + if err != nil { + return fail(ctx, 1, err.Error()) + } + return success(ctx, nil) + +} diff --git a/internal/http/controller/admin/project.go b/internal/http/controller/admin/project.go new file mode 100644 index 00000000..56960d31 --- /dev/null +++ b/internal/http/controller/admin/project.go @@ -0,0 +1,145 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/project/list", self.ProjectList) + g.POST("/crawl/project/query.html", self.ProjectQuery) + g.Match([]string{"GET", "POST"}, "/crawl/project/new", self.CrawlProject) + g.Match([]string{"GET", "POST"}, "/crawl/project/modify", self.Modify) +} + +// ProjectList 所有文章(分页) +func (ProjectController) ProjectList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), nil, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "article/list.html,article/query.html", data) +} + +// ProjectQuery +func (ProjectController) ProjectQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "domain", "title"}) + + articles, total := logic.DefaultArticle.FindArticleByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "article/query.html", data) +} + +// CrawlProject +func (ProjectController) CrawlProject(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + urls := strings.Split(ctx.FormValue("urls"), "\n") + + var errMsg string + for _, projectUrl := range urls { + err := logic.DefaultProject.ParseOneProject(strings.TrimSpace(projectUrl)) + if err != nil { + errMsg = err.Error() + } + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "project/new.html", data) +} + +// Modify +func (self ProjectController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.ProjectList))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "article/modify.html", data) + +} + +// /crawl/article/del +// func DelArticleHandler(rw http.ResponseWriter, req *http.Request) { +// var data = make(map[string]interface{}) + +// id := req.FormValue("id") + +// if _, err := strconv.Atoi(id); err != nil { +// data["ok"] = 0 +// data["error"] = "id不是整型" + +// filter.SetData(req, data) +// return +// } + +// if err := service.DelArticle(id); err != nil { +// data["ok"] = 0 +// data["error"] = "删除失败!" +// } else { +// data["ok"] = 1 +// data["msg"] = "删除成功!" +// } + +// filter.SetData(req, data) +// } diff --git a/internal/http/controller/admin/reading.go b/internal/http/controller/admin/reading.go new file mode 100644 index 00000000..c5b47fd1 --- /dev/null +++ b/internal/http/controller/admin/reading.go @@ -0,0 +1,93 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ReadingController struct{} + +// 注册路由 +func (self ReadingController) RegisterRoute(g *echo.Group) { + g.GET("/reading/list", self.ReadingList) + g.POST("/reading/query.html", self.ReadingQuery) + g.Match([]string{"GET", "POST"}, "/reading/publish", self.Publish) +} + +// ReadingList 所有晨读(分页) +func (ReadingController) ReadingList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + readings, total := logic.DefaultReading.FindReadingByPage(context.EchoContext(ctx), nil, curPage, limit) + if readings == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": readings, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "reading/list.html,reading/query.html", data) +} + +// ReadingQuery +func (ReadingController) ReadingQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"id", "rtype"}) + + readings, total := logic.DefaultReading.FindReadingByPage(context.EchoContext(ctx), conds, curPage, limit) + if readings == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": readings, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "reading/query.html", data) +} + +// Publish +func (ReadingController) Publish(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultReading.SaveReading(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + id := goutils.MustInt(ctx.QueryParam("id")) + if id != 0 { + reading := logic.DefaultReading.FindById(context.EchoContext(ctx), id) + if reading != nil { + data["reading"] = reading + } + } + + return render(ctx, "reading/modify.html", data) +} diff --git a/internal/http/controller/admin/routes.go b/internal/http/controller/admin/routes.go new file mode 100644 index 00000000..50a5701a --- /dev/null +++ b/internal/http/controller/admin/routes.go @@ -0,0 +1,24 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + g.GET("", AdminIndex) + new(AuthorityController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(NodeController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(RuleController).RegisterRoute(g) + new(ReadingController).RegisterRoute(g) + new(ToolController).RegisterRoute(g) + new(SettingController).RegisterRoute(g) + new(MetricsController).RegisterRoute(g) +} diff --git a/internal/http/controller/admin/rule.go b/internal/http/controller/admin/rule.go new file mode 100644 index 00000000..e3adf011 --- /dev/null +++ b/internal/http/controller/admin/rule.go @@ -0,0 +1,119 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type RuleController struct{} + +// 注册路由 +func (self RuleController) RegisterRoute(g *echo.Group) { + g.GET("/crawl/rule/list", self.RuleList) + g.POST("/crawl/rule/query.html", self.Query) + g.Match([]string{"GET", "POST"}, "/crawl/rule/new", self.New) + g.Match([]string{"GET", "POST"}, "/crawl/rule/modify", self.Modify) + g.POST("/crawl/rule/del", self.Del) +} + +// RuleList 所有规则(分页) +func (RuleController) RuleList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + rules, total := logic.DefaultRule.FindBy(context.EchoContext(ctx), nil, curPage, limit) + + if rules == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": rules, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "rule/list.html,rule/query.html", data) +} + +// Query +func (RuleController) Query(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"domain"}) + + rules, total := logic.DefaultRule.FindBy(context.EchoContext(ctx), conds, curPage, limit) + + if rules == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": rules, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + return render(ctx, "rule/query.html", data) +} + +// New 新建规则 +func (RuleController) New(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultRule.Save(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + return render(ctx, "rule/new.html", data) +} + +// Modify 编辑规则 +func (self RuleController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultRule.Save(context.EchoContext(ctx), forms, user.Username) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + + rule := logic.DefaultRule.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if rule == nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.RuleList))) + } + + data["rule"] = rule + + return render(ctx, "rule/modify.html", data) +} + +func (RuleController) Del(ctx echo.Context) error { + err := logic.DefaultRule.Delete(context.EchoContext(ctx), ctx.FormValue("id")) + if err != nil { + return fail(ctx, 1, "删除失败") + } + return success(ctx, nil) +} diff --git a/internal/http/controller/admin/setting.go b/internal/http/controller/admin/setting.go new file mode 100644 index 00000000..2d9fb89c --- /dev/null +++ b/internal/http/controller/admin/setting.go @@ -0,0 +1,69 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type SettingController struct{} + +// 注册路由 +func (self SettingController) RegisterRoute(g *echo.Group) { + g.Match([]string{"GET", "POST"}, "/setting/genneral/modify", self.GenneralModify) + g.Match([]string{"GET", "POST"}, "/setting/nav/modify", self.NavModify) + g.Match([]string{"GET", "POST"}, "/setting/index_tab/children", self.IndexTabChildren) +} + +// GenneralModify 常规选项修改 +func (self SettingController) GenneralModify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.Update(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + + return render(ctx, "setting/genneral.html", nil) +} + +// NavModify 菜单、导航修改 +func (self SettingController) NavModify(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.Update(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + return render(ctx, "setting/menu_nav.html", nil) +} + +func (self SettingController) IndexTabChildren(ctx echo.Context) error { + if ctx.FormValue("submit") == "1" { + forms, _ := ctx.FormParams() + err := logic.DefaultSetting.UpdateIndexTabChildren(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) + } + + tab := ctx.QueryParam("tab") + name := ctx.QueryParam("name") + + return render(ctx, "setting/index_tab.html", map[string]interface{}{"tab": tab, "name": name}) +} diff --git a/internal/http/controller/admin/tool.go b/internal/http/controller/admin/tool.go new file mode 100644 index 00000000..b7860740 --- /dev/null +++ b/internal/http/controller/admin/tool.go @@ -0,0 +1,26 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type ToolController struct{} + +// 注册路由 +func (self ToolController) RegisterRoute(g *echo.Group) { + g.GET("/tool/sitemap", self.GenSitemap) +} + +// GenSitemap +func (ToolController) GenSitemap(ctx echo.Context) error { + logic.GenSitemap() + return render(ctx, "tool/sitemap.html", nil) +} diff --git a/internal/http/controller/admin/topic.go b/internal/http/controller/admin/topic.go new file mode 100644 index 00000000..82a14140 --- /dev/null +++ b/internal/http/controller/admin/topic.go @@ -0,0 +1,93 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/community/topic/list", self.List) + g.POST("/community/topic/query.html", self.Query) + g.Match([]string{"GET", "POST"}, "/community/topic/modify", self.Modify) +} + +// List 所有主题(分页) +func (TopicController) List(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + topics, total := logic.DefaultTopic.FindByPage(context.EchoContext(ctx), nil, curPage, limit) + + if topics == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": topics, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "topic/list.html,topic/query.html", data) +} + +// Query +func (TopicController) Query(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"tid", "title", "uid"}) + + articles, total := logic.DefaultTopic.FindByPage(context.EchoContext(ctx), conds, curPage, limit) + + if articles == nil { + return ctx.HTML(http.StatusInternalServerError, "500") + } + + data := map[string]interface{}{ + "datalist": articles, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "topic/query.html", data) +} + +// Modify +func (self TopicController) Modify(ctx echo.Context) error { + var data = make(map[string]interface{}) + + if ctx.FormValue("submit") == "1" { + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) + } + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), ctx.QueryParam("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, ctx.Echo().URI(echo.HandlerFunc(self.List))) + } + + data["article"] = article + data["statusSlice"] = model.ArticleStatusSlice + data["langSlice"] = model.LangSlice + + return render(ctx, "topic/modify.html", data) +} diff --git a/internal/http/controller/admin/user.go b/internal/http/controller/admin/user.go new file mode 100644 index 00000000..5901b36d --- /dev/null +++ b/internal/http/controller/admin/user.go @@ -0,0 +1,105 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package admin + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/user/list", self.UserList) + g.POST("/user/user/query.html", self.UserQuery) + g.GET("/user/user/detail", self.Detail) + g.POST("/user/user/modify", self.Modify) + g.POST("/user/user/add_black", self.AddBlack) +} + +// UserList 所有用户(分页) +func (UserController) UserList(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + + users, total := logic.DefaultUser.FindUserByPage(context.EchoContext(ctx), nil, curPage, limit) + + data := map[string]interface{}{ + "datalist": users, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return render(ctx, "user/list.html,user/query.html", data) +} + +func (UserController) UserQuery(ctx echo.Context) error { + curPage, limit := parsePage(ctx) + conds := parseConds(ctx, []string{"uid", "username", "email"}) + + users, total := logic.DefaultUser.FindUserByPage(context.EchoContext(ctx), conds, curPage, limit) + + data := map[string]interface{}{ + "datalist": users, + "total": total, + "totalPages": (total + limit - 1) / limit, + "page": curPage, + "limit": limit, + } + + return renderQuery(ctx, "user/query.html", data) +} + +func (UserController) Detail(ctx echo.Context) error { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "uid", ctx.QueryParam("uid")) + + data := map[string]interface{}{ + "user": user, + } + + return render(ctx, "user/detail.html", data) +} + +func (UserController) Modify(ctx echo.Context) error { + uid := ctx.FormValue("uid") + + amount := goutils.MustInt(ctx.FormValue("amount")) + forms, _ := ctx.FormParams() + if amount > 0 { + logic.DefaultUserRich.Recharge(context.EchoContext(ctx), uid, forms) + } + logic.DefaultUser.AdminUpdateUser(context.EchoContext(ctx), uid, forms) + + return success(ctx, nil) +} + +func (UserController) AddBlack(ctx echo.Context) error { + uid := goutils.MustInt(ctx.FormValue("uid")) + err := logic.DefaultUser.UpdateUserStatus(context.EchoContext(ctx), uid, model.UserStatusOutage) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + // 将用户 IP 加入黑名单 + logic.DefaultRisk.AddBlackIPByUID(uid) + + truncate := goutils.MustBool(ctx.FormValue("truncate")) + if truncate { + err = logic.DefaultUser.DeleteUserContent(context.EchoContext(ctx), uid) + if err != nil { + return fail(ctx, 1, err.Error()) + } + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/app/article.go b/internal/http/controller/app/article.go new file mode 100644 index 00000000..1f22c385 --- /dev/null +++ b/internal/http/controller/app/article.go @@ -0,0 +1,80 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ArticleController struct{} + +// 注册路由 +func (this *ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/articles", this.ReadList) + g.GET("/article/detail", this.Detail) +} + +// ReadList 网友文章列表页 +func (ArticleController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + // 置顶的 article + topArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top=1") + + articles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "") + + total := logic.DefaultArticle.Count(context.EchoContext(ctx), "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "articles": append(topArticles, articles...), + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 文章详细页 +func (ArticleController) Detail(ctx echo.Context) error { + article, prevNext, err := logic.DefaultArticle.FindByIdAndPreNext(context.EchoContext(ctx), goutils.MustInt(ctx.QueryParam("id"))) + if err != nil { + return fail(ctx, err.Error()) + } + + if article == nil || article.Id == 0 || article.Status == model.ArticleStatusOffline { + return success(ctx, map[string]interface{}{"article": map[string]interface{}{"id": 0}}) + } + + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id) + + // 为了阅读数即时看到 + article.Viewnum++ + + // 回复信息(评论) + replies, _, lastReplyUser := logic.DefaultComment.FindObjComments(context.EchoContext(ctx), article.Id, model.TypeArticle, 0, article.Lastreplyuid) + // 有人回复 + if article.Lastreplyuid != 0 { + article.LastReplyUser = lastReplyUser + } + + article.Txt = "" + data := map[string]interface{}{ + "article": article, + "replies": replies, + } + + // TODO: 暂时不用 + _ = prevNext + return success(ctx, data) +} diff --git a/internal/http/controller/app/base.go b/internal/http/controller/app/base.go new file mode 100644 index 00000000..27720f3d --- /dev/null +++ b/internal/http/controller/app/base.go @@ -0,0 +1,76 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "encoding/json" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +const perPage = 12 + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "code": 0, + "msg": "ok", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + AccessControl(ctx) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, msg string, codes ...int) error { + AccessControl(ctx) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + code := 1 + if len(codes) > 0 { + code = codes[0] + } + result := map[string]interface{}{ + "code": code, + "msg": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/app/comment.go b/internal/http/controller/app/comment.go new file mode 100644 index 00000000..2067d29e --- /dev/null +++ b/internal/http/controller/app/comment.go @@ -0,0 +1,41 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type CommentController struct{} + +func (self CommentController) RegisterRoute(g *echo.Group) { + g.POST("/comment/:objid", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.PublishNotice()) +} + +// Create 评论(或回复) +func (CommentController) Create(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + + // 入库 + objid := goutils.MustInt(ctx.Param("objid")) + if objid == 0 { + return fail(ctx, "参数有误,请刷新后重试!", 1) + } + forms, _ := ctx.FormParams() + comment, err := logic.DefaultComment.Publish(context.EchoContext(ctx), user.Uid, objid, forms) + if err != nil { + return fail(ctx, "服务器内部错误", 2) + } + + return success(ctx, map[string]interface{}{"comment": comment}) +} diff --git a/websites/code/studygolang/src/controller/rss.go b/internal/http/controller/app/doc.go similarity index 51% rename from websites/code/studygolang/src/controller/rss.go rename to internal/http/controller/app/doc.go index 3c067257..7c2cc682 100644 --- a/websites/code/studygolang/src/controller/rss.go +++ b/internal/http/controller/app/doc.go @@ -2,16 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // http://studygolang.com -// Author:polaris studygolang@gmail.com +// Author: polaris polaris@studygolang.com -package controller - -import ( - "net/http" -) - -// 构建 rss 订阅 -// uri: /feed -func FeedHandler(rw http.ResponseWriter, req *http.Request) { - -} +// app Go 语言中文网 APP 接口 +package app diff --git a/internal/http/controller/app/index.go b/internal/http/controller/app/index.go new file mode 100644 index 00000000..96e5f541 --- /dev/null +++ b/internal/http/controller/app/index.go @@ -0,0 +1,77 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type IndexController struct{} + +// 注册路由 +func (self IndexController) RegisterRoute(g *echo.Group) { + g.GET("/home", self.Home) + g.GET("/stat/site", self.WebsiteStat) +} + +// Home 首页 +func (IndexController) Home(ctx echo.Context) error { + if len(logic.WebsiteSetting.IndexNavs) == 0 { + return success(ctx, nil) + } + + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "INDEX_TAB") + } + + if tab == "" { + tab = logic.WebsiteSetting.IndexNavs[0].Tab + } + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + data := logic.DefaultIndex.FindData(context.EchoContext(ctx), tab, paginator) + + SetCookie(ctx, "INDEX_TAB", data["tab"].(string)) + + data["all_nodes"] = logic.GenNodes() + + if tab == "all" { + data["total"] = paginator.GetTotal() + + } + return success(ctx, data) +} + +// WebsiteStat 网站统计信息 +func (IndexController) WebsiteStat(ctx echo.Context) error { + articleTotal := logic.DefaultArticle.Total() + projectTotal := logic.DefaultProject.Total() + topicTotal := logic.DefaultTopic.Total() + cmtTotal := logic.DefaultComment.Total() + resourceTotal := logic.DefaultResource.Total() + bookTotal := logic.DefaultGoBook.Total() + userTotal := logic.DefaultUser.Total() + + data := map[string]interface{}{ + "article": articleTotal, + "project": projectTotal, + "topic": topicTotal, + "resource": resourceTotal, + "book": bookTotal, + "comment": cmtTotal, + "user": userTotal, + } + + return success(ctx, data) +} diff --git a/internal/http/controller/app/project.go b/internal/http/controller/app/project.go new file mode 100644 index 00000000..c3cf4849 --- /dev/null +++ b/internal/http/controller/app/project.go @@ -0,0 +1,69 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/projects", self.ReadList) + g.GET("/project/detail", self.Detail) +} + +// ReadList 开源项目列表页 +func (ProjectController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", "") + + total := logic.DefaultProject.Count(context.EchoContext(ctx), "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "projects": projects, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 项目详情 +func (ProjectController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.QueryParam("id")) + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), id) + if project == nil || project.Id == 0 { + return fail(ctx, "获取失败或已下线") + } + + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id) + + // 为了阅读数即时看到 + project.Viewnum++ + + // 回复信息(评论) + replies, _, lastReplyUser := logic.DefaultComment.FindObjComments(context.EchoContext(ctx), project.Id, model.TypeProject, 0, project.Lastreplyuid) + // 有人回复 + if project.Lastreplyuid != 0 { + project.LastReplyUser = lastReplyUser + } + + return success(ctx, map[string]interface{}{ + "project": project, + "replies": replies, + }) +} diff --git a/internal/http/controller/app/resource.go b/internal/http/controller/app/resource.go new file mode 100644 index 00000000..4861f7fd --- /dev/null +++ b/internal/http/controller/app/resource.go @@ -0,0 +1,54 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ResourceController struct{} + +// 注册路由 +func (self ResourceController) RegisterRoute(g *echo.Group) { + g.GET("/resources", self.ReadList) + g.GET("/resource/detail", self.Detail) +} + +// ReadList 资源索引页 +func (ResourceController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + resources, total := logic.DefaultResource.FindAll(context.EchoContext(ctx), paginator, "resource.mtime", "") + hasMore := paginator.SetTotal(total).HasMorePage() + + data := map[string]interface{}{ + "resources": resources, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// Detail 某个资源详细页 +func (ResourceController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.QueryParam("id")) + resource, comments := logic.DefaultResource.FindById(context.EchoContext(ctx), id) + if len(resource) == 0 { + return fail(ctx, "获取失败") + } + + logic.Views.Incr(Request(ctx), model.TypeResource, id) + + return success(ctx, map[string]interface{}{"resource": resource, "comments": comments}) +} diff --git a/internal/http/controller/app/routes.go b/internal/http/controller/app/routes.go new file mode 100644 index 00000000..de79ead5 --- /dev/null +++ b/internal/http/controller/app/routes.go @@ -0,0 +1,20 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + new(IndexController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(ResourceController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(WechatController).RegisterRoute(g) + new(CommentController).RegisterRoute(g) +} diff --git a/internal/http/controller/app/topic.go b/internal/http/controller/app/topic.go new file mode 100644 index 00000000..afef2bd6 --- /dev/null +++ b/internal/http/controller/app/topic.go @@ -0,0 +1,185 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/topics", self.TopicList) + g.GET("/topics/no_reply", self.TopicsNoReply) + g.GET("/topics/last", self.TopicsLast) + g.GET("/topic/detail", self.Detail) + g.GET("/topics/node/:nid", self.NodeTopics) + + g.Match([]string{"GET", "POST"}, "/topics/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.PublishNotice()) + g.Match([]string{"GET", "POST"}, "/topics/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +func (self TopicController) TopicList(ctx echo.Context) error { + tab := ctx.QueryParam("tab") + if tab != "" && tab != "all" { + nid := logic.GetNidByEname(tab) + if nid > 0 { + return self.topicList(ctx, tab, "topics.mtime DESC", "nid=? AND top!=1", nid) + } + } + + return self.topicList(ctx, "all", "topics.mtime DESC", "top!=1") +} + +func (self TopicController) Topics(ctx echo.Context) error { + return self.topicList(ctx, "", "topics.mtime DESC", "") +} + +func (self TopicController) TopicsNoReply(ctx echo.Context) error { + return self.topicList(ctx, "no_reply", "topics.mtime DESC", "lastreplyuid=?", 0) +} + +func (self TopicController) TopicsLast(ctx echo.Context) error { + return self.topicList(ctx, "last", "ctime DESC", "") +} + +func (TopicController) topicList(ctx echo.Context, tab, orderBy, querystring string, args ...interface{}) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginatorWithPerPage(curPage, perPage) + + // 置顶的topic + topTopics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "ctime DESC", "top=1") + + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, orderBy, querystring, args...) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, args...) + hasMore := paginator.SetTotal(total).HasMorePage() + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topics": append(topTopics, topics...), + "tab": tab, + "tab_list": hotNodes, + "has_more": hasMore, + } + + return success(ctx, data) +} + +// NodeTopics 某节点下的主题列表 +func (TopicController) NodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring, nid := "nid=?", goutils.MustInt(ctx.Param("nid")) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + // 当前节点信息 + node := logic.GetNode(nid) + + return success(ctx, map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// Detail 社区主题详细页 +func (TopicController) Detail(ctx echo.Context) error { + tid := goutils.MustInt(ctx.QueryParam("tid")) + if tid == 0 { + return fail(ctx, "tid 非法") + } + + topic, replies, err := logic.DefaultTopic.FindByTid(context.EchoContext(ctx), tid) + if err != nil { + return fail(ctx, "服务器异常") + } + + me, ok := ctx.Get("user").(*model.Me) + + permission := topic["permission"].(int) + switch permission { + case model.PermissionLogin: + if !ok { + topic["content"] = "登录用户可见!" + } + case model.PermissionPay: + if !ok || !me.IsVip || !me.IsRoot { + topic["content"] = "付费用户可见!" + } + } + + logic.Views.Incr(Request(ctx), model.TypeTopic, tid) + + data := map[string]interface{}{ + "topic": topic, + "replies": replies, + } + + return success(ctx, data) +} + +// Create 新建主题 +func (TopicController) Create(ctx echo.Context) error { + nodes := logic.GenNodes() + + title := ctx.FormValue("title") + // 请求新建主题页面 + if title == "" || ctx.Request().Method != "POST" { + return success(ctx, map[string]interface{}{"nodes": nodes, "activeTopics": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + tid, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, "内部服务错误", 1) + } + + return success(ctx, map[string]interface{}{"tid": tid}) +} + +// Modify 修改主题 +func (TopicController) Modify(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + nodes := logic.GenNodes() + + if ctx.Request().Method != "POST" { + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + return success(ctx, map[string]interface{}{"nodes": nodes, "topic": topics[0], "activeTopics": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return fail(ctx, "没有权限操作", 1) + } + + return fail(ctx, "服务错误,请稍后重试!", 2) + } + return success(ctx, map[string]interface{}{"tid": tid}) +} diff --git a/internal/http/controller/app/user.go b/internal/http/controller/app/user.go new file mode 100644 index 00000000..487c285b --- /dev/null +++ b/internal/http/controller/app/user.go @@ -0,0 +1,100 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + . "github.com/studygolang/studygolang/internal/http/internal/helper" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/center", self.Center) + g.GET("/user/me", self.Me) + g.POST("/user/modify", self.Modify) + g.POST("/user/login", self.Login) +} + +// Center 用户自己个人中心 +func (UserController) Center(ctx echo.Context) error { + if user, ok := ctx.Get("user").(*model.Me); ok { + data := map[string]interface{}{ + "user": user, + } + return success(ctx, data) + } + + return success(ctx, nil) +} + +// Me 用户信息 +func (UserController) Me(ctx echo.Context) error { + if me, ok := ctx.Get("user").(*model.Me); ok { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "uid", me.Uid) + return success(ctx, map[string]interface{}{ + "user": user, + "default_avatars": logic.DefaultAvatars, + }) + } + + return success(ctx, nil) +} + +func (UserController) Login(ctx echo.Context) error { + if _, ok := ctx.Get("user").(*model.Me); ok { + return success(ctx, nil) + } + + username := ctx.FormValue("username") + if username == "" { + return fail(ctx, "用户名为空") + } + + // 处理用户登录 + passwd := ctx.FormValue("passwd") + userLogin, err := logic.DefaultUser.Login(context.EchoContext(ctx), username, passwd) + if err != nil { + return fail(ctx, err.Error()) + } + + data := map[string]interface{}{ + "token": GenToken(userLogin.Uid), + "uid": userLogin.Uid, + "username": userLogin.Username, + } + return success(ctx, data) +} + +func (UserController) Modify(ctx echo.Context) error { + me, ok := ctx.Get("user").(*model.Me) + if !ok { + return fail(ctx, "请先登录", NeedReLoginCode) + } + + forms, _ := ctx.FormParams() + + // 更新信息 + errMsg, err := logic.DefaultUser.Update(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, errMsg) + } + + email := ctx.FormValue("email") + if me.Email != email { + isHttps := CheckIsHttps(ctx) + go logic.DefaultEmail.SendActivateMail(email, RegActivateCode.GenUUID(email), isHttps) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/app/wechat.go b/internal/http/controller/app/wechat.go new file mode 100644 index 00000000..cd8b5920 --- /dev/null +++ b/internal/http/controller/app/wechat.go @@ -0,0 +1,120 @@ +// Copyright 2018 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package app + +import ( + "net/url" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type WechatController struct{} + +// RegisterRoute 注册路由 +func (self WechatController) RegisterRoute(g *echo.Group) { + g.GET("/wechat/check_session", self.CheckSession) + g.POST("/wechat/register", self.Register) + g.POST("/wechat/login", self.Login) +} + +// CheckSession 校验小程序 session +func (WechatController) CheckSession(ctx echo.Context) error { + code := ctx.QueryParam("code") + + wechatUser, err := logic.DefaultWechat.CheckSession(context.EchoContext(ctx), code) + if err != nil { + return fail(ctx, err.Error()) + } + + if wechatUser.Uid > 0 { + data := map[string]interface{}{ + "token": GenToken(wechatUser.Uid), + "uid": wechatUser.Uid, + "nickname": wechatUser.Nickname, + "avatar": wechatUser.Avatar, + } + + return success(ctx, data) + } + + data := map[string]interface{}{ + "unbind_token": GenToken(wechatUser.Id), + } + + return success(ctx, data) +} + +// Login 通过系统用户登录 +func (WechatController) Login(ctx echo.Context) error { + unbindToken := ctx.FormValue("unbind_token") + id, ok := ParseToken(unbindToken) + if !ok { + return fail(ctx, "无效请求!") + } + + username := ctx.FormValue("username") + if username == "" { + return fail(ctx, "用户名为空") + } + + // 处理用户登录 + passwd := ctx.FormValue("passwd") + userLogin, err := logic.DefaultUser.Login(context.EchoContext(ctx), username, passwd) + if err != nil { + return fail(ctx, err.Error()) + } + + userInfo := ctx.FormValue("userInfo") + + wechatUser, err := logic.DefaultWechat.Bind(context.EchoContext(ctx), id, userLogin.Uid, userInfo) + if err != nil { + return fail(ctx, err.Error()) + } + + data := map[string]interface{}{ + "token": GenToken(wechatUser.Uid), + "uid": wechatUser.Uid, + "nickname": wechatUser.Nickname, + "avatar": wechatUser.Avatar, + } + + return success(ctx, data) +} + +// Register 注册系统账号 +func (WechatController) Register(ctx echo.Context) error { + unbindToken := ctx.FormValue("unbind_token") + id, ok := ParseToken(unbindToken) + if !ok { + return fail(ctx, "无效请求!") + } + + passwd := ctx.FormValue("passwd") + pass2 := ctx.FormValue("pass2") + if passwd != pass2 { + return fail(ctx, "确认密码不一致", 1) + } + + fields := []string{"username", "email", "passwd", "userInfo"} + form := url.Values{} + for _, field := range fields { + form.Set(field, ctx.FormValue(field)) + } + form.Set("id", strconv.Itoa(id)) + + errMsg, err := logic.DefaultUser.CreateUser(context.EchoContext(ctx), form) + if err != nil { + return fail(ctx, errMsg, 2) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/article.go b/internal/http/controller/article.go new file mode 100644 index 00000000..1558163f --- /dev/null +++ b/internal/http/controller/article.go @@ -0,0 +1,228 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + "strings" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/echoutils" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeArticle, logic.ArticleComment{}) + logic.RegisterLikeObject(model.TypeArticle, logic.ArticleLike{}) +} + +type ArticleController struct{} + +// 注册路由 +func (self ArticleController) RegisterRoute(g *echo.Group) { + g.GET("/articles", self.ReadList) + g.GET("/articles/crawl", self.Crawl) + + g.GET("/articles/:id", self.Detail) + + g.Match([]string{"GET", "POST"}, "/articles/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/articles/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +// ReadList 网友文章列表页 +func (ArticleController) ReadList(ctx echo.Context) error { + limit := 20 + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + paginator.SetPerPage(limit) + total := logic.DefaultArticle.Count(context.EchoContext(ctx), "") + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + pageInfo := template.HTML(pageHtml) + + // TODO: 参考的 topics 的处理方式,但是感觉不应该这样做 + topArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top=1") + unTopArticles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", "top!=1") + articles := append(topArticles, unTopArticles...) + if articles == nil { + logger.Errorln("article controller: find article error") + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + num := len(articles) + if num == 0 { + return render(ctx, "articles/list.html", map[string]interface{}{"articles": articles, "activeArticles": "active"}) + } + + // 获取当前用户喜欢对象信息 + me, ok := ctx.Get("user").(*model.Me) + var topLikeFlags map[int]int + var unTopLikeFlags map[int]int + likeFlags := map[int]int{} + + if ok { + topArticlesNum := len(topArticles) + if topArticlesNum > 0 { + topLikeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeArticle, topArticles[0].Id, topArticles[topArticlesNum-1].Id) + for k, v := range topLikeFlags { + likeFlags[k] = v + } + } + + unTopArticlesNum := len(unTopArticles) + if unTopArticlesNum > 0 { + unTopLikeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeArticle, unTopArticles[0].Id, unTopArticles[unTopArticlesNum-1].Id) + for k, v := range unTopLikeFlags { + likeFlags[k] = v + } + } + } + + return render(ctx, "articles/list.html", map[string]interface{}{"articles": articles, "activeArticles": "active", "page": pageInfo, "likeflags": likeFlags}) +} + +// Detail 文章详细页 +func (ArticleController) Detail(ctx echo.Context) error { + article, prevNext, err := logic.DefaultArticle.FindByIdAndPreNext(context.EchoContext(ctx), goutils.MustInt(ctx.Param("id"))) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + if article == nil || article.Id == 0 || article.Status == model.ArticleStatusOffline { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + + articleGCTT := logic.DefaultArticle.FindArticleGCTT(context.EchoContext(ctx), article) + data := map[string]interface{}{ + "activeArticles": "active", + "article": article, + "article_gctt": articleGCTT, + "prev": prevNext[0], + "next": prevNext[1], + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, article.Id, model.TypeArticle) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, article.Id, model.TypeArticle) + + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id, me.Uid) + + if !article.IsSelf || me.Uid != article.User.Uid { + go logic.DefaultViewRecord.Record(article.Id, model.TypeArticle, me.Uid) + } + + if me.IsRoot || (article.IsSelf && me.Uid == article.User.Uid) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), article.Id, model.TypeArticle) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), article.Id, model.TypeArticle) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeArticle, article.Id) + } + + // 为了阅读数即时看到 + article.Viewnum++ + + data["subjects"] = logic.DefaultSubject.FindArticleSubjects(context.EchoContext(ctx), article.Id) + + return render(ctx, "articles/detail.html,common/comment.html", data) +} + +// Create 发布新文章 +func (ArticleController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + if title == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{"activeArticles": "active"} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + return render(ctx, "articles/new.html", data) + } + + if ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + forms, _ := ctx.FormParams() + id, err := logic.DefaultArticle.Publish(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "内部服务错误") + } + + return success(ctx, map[string]interface{}{"id": id}) +} + +// Modify 修改文章 +func (ArticleController) Modify(ctx echo.Context) error { + id := ctx.FormValue("id") + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), id) + + if ctx.Request().Method != "POST" { + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles/"+id) + } + + return render(ctx, "articles/new.html", map[string]interface{}{ + "article": article, + "activeArticles": "active", + }) + } + + if id == "" || ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + if err != nil { + return fail(ctx, 2, "文章不存在") + } + + me := ctx.Get("user").(*model.Me) + if !logic.CanEdit(me, article) { + return fail(ctx, 3, "没有修改权限") + } + + forms, _ := ctx.FormParams() + errMsg, err := logic.DefaultArticle.Modify(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 4, errMsg) + } + + return success(ctx, map[string]interface{}{"id": article.Id}) +} + +func (ArticleController) Crawl(ctx echo.Context) error { + strUrl := ctx.QueryParam("url") + + var ( + errMsg string + err error + ) + strUrl = strings.TrimSpace(strUrl) + _, err = logic.DefaultArticle.ParseArticle(context.EchoContext(ctx), strUrl, false) + if err != nil { + errMsg = err.Error() + } + + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + return success(ctx, nil) +} diff --git a/internal/http/controller/balance.go b/internal/http/controller/balance.go new file mode 100644 index 00000000..dbc8dab1 --- /dev/null +++ b/internal/http/controller/balance.go @@ -0,0 +1,53 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/polaris1119/goutils" + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type UserRichController struct{} + +// 注册路由 +func (self UserRichController) RegisterRoute(g *echo.Group) { + g.GET("/balance", self.MyBalance, middleware.NeedLogin()) + g.GET("/balance/add", self.Add, middleware.NeedLogin()) +} + +func (UserRichController) MyBalance(ctx echo.Context) error { + p := goutils.MustInt(ctx.QueryParam("p"), 1) + me := ctx.Get("user").(*model.Me) + balanceDetails := logic.DefaultUserRich.FindBalanceDetail(context.EchoContext(ctx), me, p) + total := logic.DefaultUserRich.Total(context.EchoContext(ctx), me.Uid) + + data := map[string]interface{}{ + "details": balanceDetails, + "total": int(total), + "cur_p": p, + } + return render(ctx, "rich/balance.html", data) +} + +func (UserRichController) Add(ctx echo.Context) error { + p := goutils.MustInt(ctx.QueryParam("p"), 1) + me := ctx.Get("user").(*model.Me) + balanceDetails := logic.DefaultUserRich.FindBalanceDetail(context.EchoContext(ctx), me, p, model.MissionTypeAdd) + + rechargeAmount := logic.DefaultUserRich.FindRecharge(context.EchoContext(ctx), me) + + data := map[string]interface{}{ + "details": balanceDetails, + "recharge_amount": rechargeAmount, + } + return render(ctx, "rich/add.html", data) +} diff --git a/internal/http/controller/base.go b/internal/http/controller/base.go new file mode 100644 index 00000000..34a264c8 --- /dev/null +++ b/internal/http/controller/base.go @@ -0,0 +1,89 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/nosql" +) + +func getLogger(ctx echo.Context) *logger.Logger { + return logic.GetLogger(context.EchoContext(ctx)) +} + +// render html 输出 +func render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + return Render(ctx, contentTpl, data) +} + +func success(ctx echo.Context, data interface{}) error { + result := map[string]interface{}{ + "ok": 1, + "msg": "操作成功", + "data": data, + } + + b, err := json.Marshal(result) + if err != nil { + return err + } + + oldETag := ctx.Request().Header.Get("If-None-Match") + if strings.HasPrefix(oldETag, "W/") { + oldETag = oldETag[2:] + } + newETag := goutils.Md5Buf(b) + if oldETag == newETag { + return ctx.NoContent(http.StatusNotModified) + } + + go func(b []byte) { + if cacheKey := ctx.Get(nosql.CacheKey); cacheKey != nil { + nosql.DefaultLRUCache.CompressAndAdd(cacheKey, b, nosql.NewCacheData()) + } + }(b) + + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + ctx.Response().Header().Add("ETag", newETag) + + callback := ctx.QueryParam("callback") + if callback != "" { + return ctx.JSONPBlob(http.StatusOK, callback, b) + } + + return ctx.JSONBlob(http.StatusOK, b) +} + +func fail(ctx echo.Context, code int, msg string) error { + if ctx.Response().Committed { + getLogger(ctx).Flush() + return nil + } + + result := map[string]interface{}{ + "ok": 0, + "error": msg, + } + + getLogger(ctx).Errorln("operate fail:", result) + + return ctx.JSON(http.StatusOK, result) +} diff --git a/internal/http/controller/book.go b/internal/http/controller/book.go new file mode 100644 index 00000000..5cfd36d8 --- /dev/null +++ b/internal/http/controller/book.go @@ -0,0 +1,116 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeBook, logic.BookComment{}) + logic.RegisterLikeObject(model.TypeBook, logic.BookLike{}) +} + +type BookController struct{} + +// 注册路由 +func (self BookController) RegisterRoute(g *echo.Group) { + g.GET("/books", self.ReadList) + + g.GET("/book/:id", self.Detail) + + g.Match([]string{"GET", "POST"}, "/book/new", self.Create, middleware.NeedLogin(), middleware.BalanceCheck(), middleware.PublishNotice()) +} + +// ReadList 图书列表页 +func (BookController) ReadList(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + books := logic.DefaultGoBook.FindAll(context.EchoContext(ctx), paginator, "likenum DESC,id DESC") + total := logic.DefaultGoBook.Count(context.EchoContext(ctx)) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + data := map[string]interface{}{ + "books": books, + "activeBooks": "active", + "page": template.HTML(pageHtml), + } + + return render(ctx, "books/list.html", data) +} + +// Create 发布新书 +func (BookController) Create(ctx echo.Context) error { + name := ctx.FormValue("name") + // 请求新建图书页面 + if name == "" || ctx.Request().Method != "POST" { + book := &model.Book{} + return render(ctx, "books/new.html", map[string]interface{}{"book": book, "activeBooks": "active"}) + } + + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultGoBook.Publish(context.EchoContext(ctx), user, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Detail 图书详细页 +func (BookController) Detail(ctx echo.Context) error { + book, err := logic.DefaultGoBook.FindById(context.EchoContext(ctx), ctx.Param("id")) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/books") + } + + if book == nil || book.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/books") + } + + data := map[string]interface{}{ + "activeBooks": "active", + "book": book, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, book.Id, model.TypeBook) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, book.Id, model.TypeBook) + + logic.Views.Incr(Request(ctx), model.TypeBook, book.Id, me.Uid) + + if me.Uid != book.Uid { + go logic.DefaultViewRecord.Record(book.Id, model.TypeBook, me.Uid) + } + + if me.IsRoot || me.Uid == book.Uid { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), book.Id, model.TypeBook) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), book.Id, model.TypeBook) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeBook, book.Id) + } + + // 为了阅读数即时看到 + book.Viewnum++ + + return render(ctx, "books/detail.html,common/comment.html", data) +} diff --git a/internal/http/controller/captcha.go b/internal/http/controller/captcha.go new file mode 100644 index 00000000..119cec78 --- /dev/null +++ b/internal/http/controller/captcha.go @@ -0,0 +1,28 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + . "github.com/studygolang/studygolang/internal/http" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" +) + +var captchaHandler = captcha.Server(100, 40) + +// 验证码 +type CaptchaController struct{} + +func (self CaptchaController) RegisterRoute(g *echo.Group) { + g.GET("/captcha/*", self.Server) +} + +func (CaptchaController) Server(ctx echo.Context) error { + captchaHandler.ServeHTTP(ResponseWriter(ctx), Request(ctx)) + return nil +} diff --git a/internal/http/controller/comment.go b/internal/http/controller/comment.go new file mode 100644 index 00000000..a0d40168 --- /dev/null +++ b/internal/http/controller/comment.go @@ -0,0 +1,204 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "errors" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" + "github.com/studygolang/studygolang/echoutils" +) + +// 在需要喜欢且要回调的地方注册喜欢对象 +func init() { + // 注册喜欢对象 + logic.RegisterLikeObject(model.TypeComment, logic.CommentLike{}) +} + +type CommentController struct{} + +func (self CommentController) RegisterRoute(g *echo.Group) { + g.GET("/at/users", self.AtUsers) + g.POST("/comment/:objid", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice()) + g.GET("/object/comments", self.CommentList) + g.POST("/object/comments/:cid", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + + g.GET("/topics/:objid/comment/:cid", self.TopicDetail) + g.GET("/articles/:objid/comment/:cid", self.ArticleDetail) +} + +// AtUsers 评论或回复 @ 某人 suggest +func (CommentController) AtUsers(ctx echo.Context) error { + term := ctx.QueryParam("term") + isHttps := CheckIsHttps(ctx) + users := logic.DefaultUser.GetUserMentions(term, 10, isHttps) + return ctx.JSON(http.StatusOK, users) +} + +// Create 评论(或回复) +func (CommentController) Create(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + + // 入库 + objid := goutils.MustInt(ctx.Param("objid")) + if objid == 0 { + return fail(ctx, 1, "参数有误,请刷新后重试!") + } + forms, _ := ctx.FormParams() + comment, err := logic.DefaultComment.Publish(context.EchoContext(ctx), user.Uid, objid, forms) + if err != nil { + return fail(ctx, 2, "服务器内部错误") + } + + return success(ctx, comment) +} + +// 修改评论 +func (CommentController) Modify(ctx echo.Context) error { + cid := goutils.MustInt(ctx.Param("cid")) + content := ctx.FormValue("content") + comment, err := logic.DefaultComment.FindById(cid) + + if err != nil { + return fail(ctx, 2, "评论不存在") + } + + if content == "" { + return fail(ctx, 1, "内容不能为空") + } + + me := ctx.Get("user").(*model.Me) + // CanEdit 已包含修改时间限制 + if !logic.CanEdit(me, comment) { + return fail(ctx, 3, "没有修改权限") + } + + errMsg, err := logic.DefaultComment.Modify(echoutils.WrapEchoContext(ctx), cid, content) + if err != nil { + return fail(ctx, 4, errMsg) + } + + return success(ctx, map[string]interface{}{"cid": cid}) +} + +// CommentList 获取某对象的评论信息 +func (CommentController) CommentList(ctx echo.Context) error { + objid := goutils.MustInt(ctx.QueryParam("objid")) + objtype := goutils.MustInt(ctx.QueryParam("objtype")) + p := goutils.MustInt(ctx.QueryParam("p")) + + commentList, replyComments, pageNum, err := logic.DefaultComment.FindObjectComments(context.EchoContext(ctx), objid, objtype, p) + if err != nil { + return fail(ctx, 1, "服务器内部错误") + } + + uids := slices.StructsIntSlice(commentList, "Uid") + if len(replyComments) > 0 { + replyUids := slices.StructsIntSlice(replyComments, "Uid") + uids = append(uids, replyUids...) + } + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + result := map[string]interface{}{ + "comments": commentList, + "page_num": pageNum, + } + + // json encode 不支持 map[int]... + for uid, user := range users { + result[strconv.Itoa(uid)] = user + } + + replyMap := make(map[string]interface{}) + for _, comment := range replyComments { + replyMap[strconv.Itoa(comment.Floor)] = comment + } + result["reply_comments"] = replyMap + + return success(ctx, result) +} + +func (self CommentController) TopicDetail(ctx echo.Context) error { + objid := goutils.MustInt(ctx.Param("objid")) + cid := goutils.MustInt(ctx.Param("cid")) + + topicMaps := logic.DefaultTopic.FindFullinfoByTids([]int{objid}) + if len(topicMaps) < 1 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topic := topicMaps[0] + topic["node"] = logic.GetNode(topic["nid"].(int)) + + data := map[string]interface{}{ + "topic": topic, + } + data["appends"] = logic.DefaultTopic.FindAppend(context.EchoContext(ctx), objid) + + err := self.fillCommentAndUser(ctx, data, cid, objid, model.TypeTopic) + + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/topics/"+strconv.Itoa(objid)) + } + + return render(ctx, "topics/comment.html", data) +} + +func (self CommentController) ArticleDetail(ctx echo.Context) error { + objid := goutils.MustInt(ctx.Param("objid")) + cid := goutils.MustInt(ctx.Param("cid")) + + article, err := logic.DefaultArticle.FindById(context.EchoContext(ctx), objid) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles") + } + articleGCTT := logic.DefaultArticle.FindArticleGCTT(context.EchoContext(ctx), article) + + data := map[string]interface{}{ + "article": article, + "article_gctt": articleGCTT, + } + + err = self.fillCommentAndUser(ctx, data, cid, objid, model.TypeArticle) + + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/articles/"+strconv.Itoa(objid)) + } + + return render(ctx, "articles/comment.html", data) +} + +func (CommentController) fillCommentAndUser(ctx echo.Context, data map[string]interface{}, cid, objid, objtype int) error { + comment, comments := logic.DefaultComment.FindComment(context.EchoContext(ctx), cid, objid, objtype) + + if comment.Cid == 0 { + return errors.New("comment not exists!") + } + + uids := make([]int, 1+len(comments)) + uids[0] = comment.Uid + for i, comment := range comments { + uids[i+1] = comment.Uid + } + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + data["comment"] = comment + data["comments"] = comments + data["users"] = users + + return nil +} diff --git a/internal/http/controller/download.go b/internal/http/controller/download.go new file mode 100644 index 00000000..ddab8e44 --- /dev/null +++ b/internal/http/controller/download.go @@ -0,0 +1,149 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" +) + +const GoStoragePrefix = "https://golang.google.cn/dl/" + +type DownloadController struct{} + +// 注册路由 +func (self DownloadController) RegisterRoute(g *echo.Group) { + g.GET("/dl", self.GoDl) + g.Match([]string{"GET", "HEAD"}, "/dl/golang/:filename", self.FetchGoInstallPackage) + g.GET("/dl/add_new_version", self.AddNewDownload) +} + +// GoDl Go 语言安装包下载 +func (DownloadController) GoDl(ctx echo.Context) error { + downloads := logic.DefaultDownload.FindAll(context.EchoContext(ctx)) + + featured := make([]*model.Download, 0, 5) + stables := make(map[string][]*model.Download) + stableVersions := make([]string, 0, 2) + unstables := make(map[string][]*model.Download) + archiveds := make(map[string][]*model.Download) + archivedVersions := make([]string, 0, 20) + + for _, download := range downloads { + version := download.Version + if download.Category == model.DLStable { + if _, ok := stables[version]; !ok { + stableVersions = append(stableVersions, version) + stables[version] = make([]*model.Download, 0, 15) + } + stables[version] = append(stables[version], download) + + if download.IsRecommend && len(featured) < 5 { + featured = append(featured, download) + } + } else if download.Category == model.DLUnstable { + if _, ok := unstables[version]; !ok { + unstables[version] = make([]*model.Download, 0, 15) + } + unstables[version] = append(unstables[version], download) + } else if download.Category == model.DLArchived { + if _, ok := archiveds[version]; !ok { + archivedVersions = append(archivedVersions, version) + archiveds[version] = make([]*model.Download, 0, 15) + } + archiveds[version] = append(archiveds[version], download) + } + } + + data := map[string]interface{}{ + "activeDl": "active", + "featured": featured, + "stables": stables, + "stable_versions": stableVersions, + "unstables": unstables, + "archiveds": archiveds, + "archived_versions": archivedVersions, + } + + return render(ctx, "download/go.html", data) +} + +var filenameReg = regexp.MustCompile(`\d+\.\d[a-z\.]*\d+`) + +func (self DownloadController) FetchGoInstallPackage(ctx echo.Context) error { + filename := ctx.Param("filename") + + go logic.DefaultDownload.RecordDLTimes(context.EchoContext(ctx), filename) + + officalUrl := GoStoragePrefix + filename + resp, err := self.headWithTimeout(officalUrl) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return ctx.Redirect(http.StatusSeeOther, officalUrl) + } + if err == nil { + resp.Body.Close() + } + + goVersion := filenameReg.FindString(filename) + filePath := fmt.Sprintf("go/%s/%s", goVersion, filename) + + dlUrls := strings.Split(config.ConfigFile.MustValue("download", "dl_urls"), ",") + for _, dlUrl := range dlUrls { + dlUrl += filePath + resp, err = self.headWithTimeout(dlUrl) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return ctx.Redirect(http.StatusSeeOther, dlUrl) + } + if err == nil { + resp.Body.Close() + } + } + + getLogger(ctx).Infoln("download:", filename, "from the site static directory") + + return ctx.Redirect(http.StatusSeeOther, "/static/"+filePath) +} + +func (DownloadController) AddNewDownload(ctx echo.Context) error { + version := ctx.QueryParam("version") + selector := ctx.QueryParam("selector") + + if version == "" { + return fail(ctx, 1, "version is empty") + } + + if selector == "" { + selector = ".toggleVisible" + } + + err := logic.DefaultDownload.AddNewDownload(context.EchoContext(ctx), version, selector) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +func (DownloadController) headWithTimeout(dlUrl string) (*http.Response, error) { + client := http.Client{ + Timeout: 5 * time.Second, + } + + return client.Head(dlUrl) +} diff --git a/internal/http/controller/favorite.go b/internal/http/controller/favorite.go new file mode 100644 index 00000000..01cdf577 --- /dev/null +++ b/internal/http/controller/favorite.go @@ -0,0 +1,93 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" +) + +type FavoriteController struct{} + +// 注册路由 +func (self FavoriteController) RegisterRoute(g *echo.Group) { + g.POST("/favorite/:objid", self.Create, middleware.NeedLogin()) + g.GET("/favorites/:username", self.ReadList) +} + +// Create 收藏(取消收藏) +func (FavoriteController) Create(ctx echo.Context) error { + objtype := goutils.MustInt(ctx.FormValue("objtype")) + objid := goutils.MustInt(ctx.Param("objid")) + collect := goutils.MustInt(ctx.FormValue("collect")) + + user := ctx.Get("user").(*model.Me) + + var err error + if collect == 1 { + err = logic.DefaultFavorite.Save(context.EchoContext(ctx), user.Uid, objid, objtype) + } else { + err = logic.DefaultFavorite.Cancel(context.EchoContext(ctx), user.Uid, objid, objtype) + } + + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// ReadList 我的(某人的)收藏 +func (FavoriteController) ReadList(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + objtype := goutils.MustInt(ctx.QueryParam("objtype"), model.TypeArticle) + p := goutils.MustInt(ctx.QueryParam("p"), 1) + + data := map[string]interface{}{"objtype": objtype, "user": user} + + rows := goutils.MustInt(ctx.QueryParam("rows"), 20) + if rows > 20 { + rows = 20 + } + favorites, total := logic.DefaultFavorite.FindUserFavorites(context.EchoContext(ctx), user.Uid, objtype, (p-1)*rows, rows) + if total > 0 { + objids := slices.StructsIntSlice(favorites, "Objid") + + switch objtype { + case model.TypeTopic: + data["topics"] = logic.DefaultTopic.FindByTids(objids) + case model.TypeArticle: + data["articles"] = logic.DefaultArticle.FindByIds(objids) + case model.TypeResource: + data["resources"] = logic.DefaultResource.FindByIds(objids) + case model.TypeWiki: + // data["wikis"] = logic.DefaultWiki.FindWikisByIds(objids) + case model.TypeProject: + data["projects"] = logic.DefaultProject.FindByIds(objids) + } + } + + uri := fmt.Sprintf("/favorites/%s?objtype=%d&rows=%d&", user.Username, objtype, rows) + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(total).GetPageHtml(uri) + + return render(ctx, "favorite.html", data) +} diff --git a/internal/http/controller/feed.go b/internal/http/controller/feed.go new file mode 100644 index 00000000..ea2e4e22 --- /dev/null +++ b/internal/http/controller/feed.go @@ -0,0 +1,101 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "net/http" + "time" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + "github.com/gorilla/feeds" + echo "github.com/labstack/echo/v4" +) + +type FeedController struct{} + +// 注册路由 +func (self FeedController) RegisterRoute(g *echo.Group) { + g.GET("/feed.html", self.Atom) + g.GET("/feed.xml", self.List) +} + +func (self FeedController) Atom(ctx echo.Context) error { + return Render(ctx, "atom.html", map[string]interface{}{}) +} + +func (self FeedController) List(ctx echo.Context) error { + link := logic.WebsiteSetting.Domain + if logic.WebsiteSetting.OnlyHttps { + link = "https://" + link + "/" + } else { + link = "http://" + link + "/" + } + + now := time.Now() + + feed := &feeds.Feed{ + Title: logic.WebsiteSetting.Name, + Link: &feeds.Link{Href: link}, + Description: logic.WebsiteSetting.Slogan, + Author: &feeds.Author{Name: "polaris", Email: "polaris@studygolang.com"}, + Created: now, + Updated: now, + } + + respBody, err := logic.DefaultSearcher.FindAtomFeeds(50) + if err != nil { + return err + } + + feed.Items = make([]*feeds.Item, len(respBody.Docs)) + + for i, doc := range respBody.Docs { + url := "" + + switch doc.Objtype { + case model.TypeTopic: + url = fmt.Sprintf("%stopics/%d", link, doc.Objid) + case model.TypeArticle: + url = fmt.Sprintf("%sarticles/%d", link, doc.Objid) + case model.TypeResource: + url = fmt.Sprintf("%sresources/%d", link, doc.Objid) + case model.TypeProject: + url = fmt.Sprintf("%sp/%d", link, doc.Objid) + case model.TypeWiki: + url = fmt.Sprintf("%swiki/%d", link, doc.Objid) + case model.TypeBook: + url = fmt.Sprintf("%sbook/%d", link, doc.Objid) + } + feed.Items[i] = &feeds.Item{ + Title: doc.Title, + Link: &feeds.Link{Href: url}, + Author: &feeds.Author{Name: doc.Author}, + Description: doc.Content, + Created: time.Time(doc.CreatedAt), + Updated: time.Time(doc.CreatedAt), + } + } + + atom, err := feed.ToAtom() + if err != nil { + return err + } + + return self.responseXML(ctx, atom) +} + +func (FeedController) responseXML(ctx echo.Context, data string) (err error) { + response := ctx.Response() + response.Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + response.WriteHeader(http.StatusOK) + _, err = response.Write([]byte(data)) + return +} diff --git a/internal/http/controller/gctt.go b/internal/http/controller/gctt.go new file mode 100644 index 00000000..9af685e3 --- /dev/null +++ b/internal/http/controller/gctt.go @@ -0,0 +1,237 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "crypto/hmac" + "crypto/sha1" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/studygolang/studygolang/echoutils" +) + +type GCTTController struct{} + +// 注册路由 +func (self GCTTController) RegisterRoute(g *echo.Group) { + g.GET("/gctt", self.Index) + g.GET("/gctt-list", self.UserList) + g.GET("/gctt-issue", self.IssueList) + g.GET("/gctt/:username", self.User) + g.GET("/gctt-apply", self.Apply, middleware.NeedLogin()) + g.Match([]string{"GET", "POST"}, "/gctt-new", self.Create, middleware.NeedLogin()) + + g.POST("/gctt-webhook", self.Webhook) +} + +func (self GCTTController) Index(ctx echo.Context) error { + gcttTimeLines := logic.DefaultGCTT.FindTimeLines(context.EchoContext(ctx)) + gcttUsers := logic.DefaultGCTT.FindCoreUsers(context.EchoContext(ctx)) + gcttIssues := logic.DefaultGCTT.FindUnTranslateIssues(context.EchoContext(ctx), 10) + + return Render(ctx, "gctt/index.html", map[string]interface{}{ + "time_lines": gcttTimeLines, + "users": gcttUsers, + "issues": gcttIssues, + }) +} + +// Apply 申请成为译者 +func (GCTTController) Apply(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + gcttUser := logic.DefaultGCTT.FindTranslator(context.EchoContext(ctx), me) + if gcttUser.Id > 0 { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + // 是否绑定了 github 账号 + var githubUser *model.BindUser + bindUsers := logic.DefaultUser.FindBindUsers(context.EchoContext(ctx), me.Uid) + for _, bindUser := range bindUsers { + if bindUser.Type == model.BindTypeGithub { + githubUser = bindUser + break + } + } + + // 如果已经绑定,查看是否之前已经是译者 + if githubUser != nil { + gcttUser = logic.DefaultGCTT.FindOne(context.EchoContext(ctx), githubUser.Username) + logic.DefaultGCTT.BindUser(context.EchoContext(ctx), gcttUser, me.Uid, githubUser) + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + return render(ctx, "gctt/apply.html", map[string]interface{}{ + "activeGCTT": "active", + "github_user": githubUser, + }) +} + +// Create 发布新译文 +func (GCTTController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + gcttUser := logic.DefaultGCTT.FindTranslator(context.EchoContext(ctx), me) + + title := ctx.FormValue("title") + if title == "" || ctx.Request().Method != "POST" { + return render(ctx, "gctt/new.html", map[string]interface{}{ + "activeGCTT": "active", + "gctt_user": gcttUser, + }) + } + + if ctx.FormValue("content") == "" { + return fail(ctx, 1, "内容不能为空") + } + + if gcttUser == nil { + return fail(ctx, 2, "不允许发布!") + } + + forms, _ := ctx.FormParams() + id, err := logic.DefaultArticle.Publish(echoutils.WrapEchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 3, "内部服务错误") + } + + return success(ctx, map[string]interface{}{"id": id}) +} + +func (GCTTController) User(ctx echo.Context) error { + username := ctx.Param("username") + if username == "" { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + gcttUser := logic.DefaultGCTT.FindOne(context.EchoContext(ctx), username) + if gcttUser.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/gctt") + } + + joinDays := int(gcttUser.LastAt-gcttUser.JoinedAt)/86400 + 1 + avgDays := fmt.Sprintf("%.1f", float64(gcttUser.AvgTime)/86400.0) + + articles := logic.DefaultArticle.FindTaGCTTArticles(context.EchoContext(ctx), username) + + return render(ctx, "gctt/user-info.html", map[string]interface{}{ + "gctt_user": gcttUser, + "articles": articles, + "join_days": joinDays, + "avg_days": avgDays, + }) +} + +func (GCTTController) UserList(ctx echo.Context) error { + users := logic.DefaultGCTT.FindUsers(context.EchoContext(ctx)) + + num, words := 0, 0 + for _, user := range users { + num += user.Num + words += user.Words + } + + prs := logic.DefaultGCTT.FindNewestGit(context.EchoContext(ctx)) + + return render(ctx, "gctt/user-list.html", map[string]interface{}{ + "users": users, + "num": num, + "words": words, + "prs": prs, + }) +} + +func (GCTTController) IssueList(ctx echo.Context) error { + querystring, arg := "", "" + + label := ctx.QueryParam("label") + + translator := ctx.QueryParam("translator") + if translator != "" { + querystring = "translator=?" + arg = translator + } else { + if label == model.LabelUnClaim { + querystring = "label=?" + arg = label + } else if label == model.LabelClaimed { + querystring = "label=? AND state=" + strconv.Itoa(model.IssueOpened) + arg = label + } + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + issues := logic.DefaultGCTT.FindIssues(context.EchoContext(ctx), paginator, querystring, arg) + + total := logic.DefaultGCTT.IssueCount(context.EchoContext(ctx), querystring, arg) + pageHTML := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + prs := logic.DefaultGCTT.FindNewestGit(context.EchoContext(ctx)) + + return render(ctx, "gctt/issue-list.html", map[string]interface{}{ + "issues": issues, + "prs": prs, + "page": template.HTML(pageHTML), + "translator": translator, + "label": label, + "total": total, + }) +} + +func (GCTTController) Webhook(ctx echo.Context) error { + body, err := ioutil.ReadAll(Request(ctx).Body) + if err != nil { + logger.Errorln("GCTTController Webhook error:", err) + return err + } + + header := ctx.Request().Header + + tokenSecret := config.ConfigFile.MustValue("gctt", "token_secret") + ok := checkMAC(body, header.Get("X-Hub-Signature"), []byte(tokenSecret)) + if !ok { + logger.Errorln("GCTTController Webhook checkMAC error", string(body)) + return nil + } + + event := header.Get("X-GitHub-Event") + logger.Infoln("GCTTController Webhook event:", event) + switch event { + case "pull_request": + return logic.DefaultGithub.PullRequestEvent(context.EchoContext(ctx), body) + case "issue_comment": + return logic.DefaultGithub.IssueCommentEvent(context.EchoContext(ctx), body) + case "issues": + return logic.DefaultGithub.IssueEvent(context.EchoContext(ctx), body) + default: + fmt.Println("not deal event:", event) + } + + return nil +} + +func checkMAC(message []byte, messageMAC string, key []byte) bool { + mac := hmac.New(sha1.New, key) + mac.Write(message) + expectedMAC := fmt.Sprintf("%x", mac.Sum(nil)) + return messageMAC == "sha1="+expectedMAC +} diff --git a/internal/http/controller/gift.go b/internal/http/controller/gift.go new file mode 100644 index 00000000..68af83fc --- /dev/null +++ b/internal/http/controller/gift.go @@ -0,0 +1,66 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type GiftController struct{} + +// 注册路由 +func (self GiftController) RegisterRoute(g *echo.Group) { + g.GET("/gift", self.GiftList) + g.POST("/gift/exchange", self.Exchange, middleware.NeedLogin()) + g.GET("/gift/mine", self.MyGift, middleware.NeedLogin()) +} + +func (GiftController) GiftList(ctx echo.Context) error { + gifts := logic.DefaultGift.FindAllOnline(context.EchoContext(ctx)) + + if len(gifts) > 0 { + user, ok := ctx.Get("user").(*model.Me) + if ok { + logic.DefaultGift.UserCanExchange(context.EchoContext(ctx), user, gifts) + } + } + + data := map[string]interface{}{ + "gifts": gifts, + } + + return render(ctx, "gift/list.html", data) +} + +func (GiftController) Exchange(ctx echo.Context) error { + giftId := goutils.MustInt(ctx.FormValue("gift_id")) + me := ctx.Get("user").(*model.Me) + err := logic.DefaultGift.Exchange(context.EchoContext(ctx), me, giftId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +func (GiftController) MyGift(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + exchangeRecords := logic.DefaultGift.FindExchangeRecords(context.EchoContext(ctx), me) + + data := map[string]interface{}{ + "records": exchangeRecords, + } + + return render(ctx, "gift/mine.html", data) +} diff --git a/internal/http/controller/image.go b/internal/http/controller/image.go new file mode 100644 index 00000000..6c05853a --- /dev/null +++ b/internal/http/controller/image.go @@ -0,0 +1,212 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "encoding/json" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/times" +) + +// 图片处理 +type ImageController struct{} + +func (self ImageController) RegisterRoute(g *echo.Group) { + // todo 这三个upload差不多啊 + g.POST("/image/upload", self.Upload) + g.POST("/image/paste_upload", self.PasteUpload) + g.POST("/image/quick_upload", self.QuickUpload) + g.Match([]string{"GET", "POST"}, "/image/transfer", self.Transfer) +} + +// PasteUpload jquery 粘贴上传图片 +func (self ImageController) PasteUpload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("imageFile") + if err != nil { + objLogger.Errorln("upload error:", err) + return self.pasteUploadFail(ctx, err.Error()) + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return self.pasteUploadFail(ctx, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return self.pasteUploadFail(ctx, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return self.pasteUploadFail(ctx, "文件太大!") + } + + imgDir := times.Format("ymd") + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return self.pasteUploadFail(ctx, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + data := map[string]interface{}{ + "success": 1, + "message": cdnDomain + path, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +// QuickUpload CKEditor 编辑器,上传图片,支持粘贴方式上传 +func (self ImageController) QuickUpload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("upload") + if err != nil { + objLogger.Errorln("upload error:", err) + return self.quickUploadFail(ctx, err.Error()) + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return self.quickUploadFail(ctx, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return self.quickUploadFail(ctx, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return self.quickUploadFail(ctx, "文件太大!") + } + + fileName := goutils.Md5Buf(buf) + filepath.Ext(fileHeader.Filename) + imgDir := times.Format("ymd") + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return self.quickUploadFail(ctx, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + data := map[string]interface{}{ + "uploaded": 1, + "fileName": fileName, + "url": cdnDomain + path, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +// Upload 上传图片 +func (ImageController) Upload(ctx echo.Context) error { + objLogger := getLogger(ctx) + + file, fileHeader, err := Request(ctx).FormFile("img") + if err != nil { + objLogger.Errorln("upload error:", err) + return fail(ctx, 1, "非法文件上传!") + } + defer file.Close() + + // 如果是临时文件,存在硬盘中,则是 *os.File(大于32M),直接报错 + if _, ok := file.(*os.File); ok { + objLogger.Errorln("upload error:file too large!") + return fail(ctx, 2, "文件太大!") + } + + buf, err := ioutil.ReadAll(file) + if err != nil { + return fail(ctx, 3, "文件读取失败!") + } + if len(buf) > logic.MaxImageSize { + return fail(ctx, 4, "文件太大!") + } + + imgDir := times.Format("ymd") + if ctx.FormValue("avatar") != "" { + imgDir = "avatar" + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + file.Seek(0, io.SeekStart) + path, err := logic.DefaultUploader.UploadImage(context.EchoContext(ctx), file, imgDir, buf, filepath.Ext(fileHeader.Filename)) + if err != nil { + return fail(ctx, 5, "文件上传失败!") + } + + return success(ctx, map[string]interface{}{"url": cdnDomain + path, "uri": path}) +} + +// Transfer 转换图片:通过 url 从远程下载图片然后转存到七牛 +func (ImageController) Transfer(ctx echo.Context) error { + origUrl := ctx.FormValue("url") + if origUrl == "" { + return fail(ctx, 1, "url不能为空!") + } + + path, err := logic.DefaultUploader.TransferUrl(context.EchoContext(ctx), origUrl) + if err != nil { + return fail(ctx, 2, "文件上传失败!") + } + + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + + return success(ctx, map[string]interface{}{"url": cdnDomain + path}) +} + +func (ImageController) quickUploadFail(ctx echo.Context, message string) error { + data := map[string]interface{}{ + "uploaded": 0, + "error": map[string]string{ + "message": message, + }, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} + +func (ImageController) pasteUploadFail(ctx echo.Context, message string) error { + data := map[string]interface{}{ + "success": 0, + "message": message, + } + b, err := json.Marshal(data) + if err != nil { + return err + } + return ctx.JSONBlob(http.StatusOK, b) +} diff --git a/internal/http/controller/index.go b/internal/http/controller/index.go new file mode 100644 index 00000000..a37f7079 --- /dev/null +++ b/internal/http/controller/index.go @@ -0,0 +1,161 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "bytes" + "html/template" + "net/http" + "net/url" + "strings" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +type IndexController struct{} + +// 注册路由 +func (self IndexController) RegisterRoute(g *echo.Group) { + g.GET("/", self.Index) + g.GET("/wr", self.WrapUrl) + g.GET("/pkgdoc", self.Pkgdoc) + g.GET("/markdown", self.Markdown) + g.GET("/link", self.Link) +} + +func (IndexController) Index(ctx echo.Context) error { + if len(logic.WebsiteSetting.IndexNavs) == 0 { + return render(ctx, "index.html", nil) + } + + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "INDEX_TAB") + } + + if tab == "" { + tab = logic.WebsiteSetting.IndexNavs[0].Tab + } + paginator := logic.NewPaginator(goutils.MustInt(ctx.QueryParam("p"), 1)) + + data := logic.DefaultIndex.FindData(context.EchoContext(ctx), tab, paginator) + + SetCookie(ctx, "INDEX_TAB", data["tab"].(string)) + + data["all_nodes"] = logic.GenNodes() + + if tab == model.TabAll || tab == model.TabRecommend { + pageHtml := paginator.SetTotal(logic.DefaultFeed.GetTotalCount(context.EchoContext(ctx))).GetPageHtml(ctx.Request().URL.Path) + + data["page"] = template.HTML(pageHtml) + + data["total"] = paginator.GetTotal() + } + + return render(ctx, "index.html", data) +} + +// WrapUrl 包装链接 +func (IndexController) WrapUrl(ctx echo.Context) error { + tUrl := ctx.QueryParam("u") + if tUrl == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 本站 + if strings.Contains(tUrl, logic.WebsiteSetting.Domain) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + if strings.Contains(tUrl, "?") { + tUrl += "&" + } else { + tUrl += "?" + } + tUrl += "utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com" + + if CheckIsHttps(ctx) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + var ( + pUrl *url.URL + err error + ) + + if pUrl, err = url.Parse(tUrl); err != nil { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + + iframeDeny := config.ConfigFile.MustValue("crawl", "iframe_deny") + // 检测是否禁止了 iframe 加载 + // 看是否在黑名单中 + for _, denyHost := range strings.Split(iframeDeny, ",") { + if strings.Contains(pUrl.Host, denyHost) { + return ctx.Redirect(http.StatusSeeOther, tUrl) + } + } + + // 检测会比较慢,进行异步检测,记录下来,以后分析再加黑名单 + go func() { + resp, err := http.Head(tUrl) + if err != nil { + logger.Errorln("[iframe] head url:", tUrl, "error:", err) + return + } + defer resp.Body.Close() + if resp.Header.Get("X-Frame-Options") != "" { + logger.Errorln("[iframe] deny:", tUrl) + return + } + }() + + return render(ctx, "wr.html", map[string]interface{}{"url": tUrl}) +} + +// PkgdocHandler Go 语言文档中文版 +func (IndexController) Pkgdoc(ctx echo.Context) error { + // return render(ctx, "pkgdoc.html", map[string]interface{}{"activeDoc": "active"}) + tpl, err := template.ParseFiles(config.TemplateDir + "pkgdoc.html") + if err != nil { + logger.Errorln("parse file error:", err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, nil) + if err != nil { + logger.Errorln("execute template error:", err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func (IndexController) Markdown(ctx echo.Context) error { + return render(ctx, "markdown.html", nil) +} + +// Link 用于重定向外部链接,比如广告链接 +func (IndexController) Link(ctx echo.Context) error { + tUrl := ctx.QueryParam("url") + if strings.Contains(tUrl, "?") { + tUrl += "&" + } else { + tUrl += "?" + } + tUrl += "utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com" + return ctx.Redirect(http.StatusSeeOther, tUrl) +} diff --git a/internal/http/controller/install.go b/internal/http/controller/install.go new file mode 100644 index 00000000..8d3da960 --- /dev/null +++ b/internal/http/controller/install.go @@ -0,0 +1,297 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "bytes" + "html/template" + "net/http" + "net/url" + "runtime" + "strconv" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/db" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" +) + +type InstallController struct{} + +// 注册路由 +func (self InstallController) RegisterRoute(g *echo.Group) { + g.GET("/install", self.SetupConfig) + g.Match([]string{"GET", "POST"}, "/install/setup-config", self.SetupConfig) + g.Match([]string{"GET", "POST"}, "/install/do", self.DoInstall) + g.Match([]string{"GET", "POST"}, "/install/options", self.SetupOptions) +} + +func (self InstallController) SetupConfig(ctx echo.Context) error { + // config/env.ini 存在 + if db.MasterDB != nil { + if logic.DefaultInstall.IsTableExist(context.EchoContext(ctx)) { + return ctx.Redirect(http.StatusSeeOther, "/") + } + return ctx.Redirect(http.StatusSeeOther, "/install/do") + } + + step := goutils.MustInt(ctx.QueryParam("step")) + if step == 2 { + err := self.genConfig(ctx) + if err != nil { + data := map[string]interface{}{ + "dbhost": ctx.FormValue("dbhost"), + "dbport": ctx.FormValue("dbport"), + "dbname": ctx.FormValue("dbname"), + "uname": ctx.FormValue("uname"), + "err_type": 1, + } + + if err == db.ConnectDBErr { + data["err_type"] = 1 + } else if err == db.UseDBErr { + data["err_type"] = 2 + } + + return renderInstall(ctx, "install/setup-err.html", data) + } + } + return renderInstall(ctx, "install/setup-config.html", map[string]interface{}{"step": step}) +} + +// DoInstall 执行安装,包括站点简单配置,安装数据库(创建数据库、表,填充基本数据)等 +func (self InstallController) DoInstall(ctx echo.Context) error { + if db.MasterDB == nil { + return ctx.Redirect(http.StatusSeeOther, "/install") + } + + if logic.DefaultInstall.IsTableExist(context.EchoContext(ctx)) { + if logic.DefaultInstall.HadRootUser(context.EchoContext(ctx)) { + return ctx.Redirect(http.StatusSeeOther, "/") + } + } + + step := goutils.MustInt(ctx.QueryParam("step"), 1) + data := map[string]interface{}{ + "user_name": "admin", + "admin_email": "", + "step": step, + } + + if step == 2 { + username := ctx.FormValue("user_name") + email := ctx.FormValue("admin_email") + password1 := ctx.FormValue("admin_password") + password2 := ctx.FormValue("admin_password2") + + if username == "" || email == "" { + data["err"] = "用户名和邮箱不能留空" + return renderInstall(ctx, "install/install.html", data) + } + + data["user_name"] = username + data["admin_email"] = email + + if password1 != password2 { + data["err"] = "两次输入的密码不一致" + return renderInstall(ctx, "install/install.html", data) + } + + err := logic.DefaultInstall.CreateTable(context.EchoContext(ctx)) + if err != nil { + data["err"] = "创建数据表失败!" + return renderInstall(ctx, "install/install.html", data) + } + + err = logic.DefaultInstall.InitTable(context.EchoContext(ctx)) + if err != nil { + data["err"] = "初始化数据表失败!" + return renderInstall(ctx, "install/install.html", data) + } + + if password1 == "" { + password1 = goutils.RandString(12) + data["passwd"] = password1 + } + + // 创建管理员 + form := url.Values{ + "username": {username}, + "email": {email}, + "passwd": {password1}, + "is_root": {"true"}, + "status": {strconv.Itoa(model.UserStatusAudit)}, + } + errMsg, err := logic.DefaultUser.CreateUser(context.EchoContext(ctx), form) + if err != nil { + data["err"] = errMsg + return renderInstall(ctx, "install/install.html", data) + } + + data["step"] = 3 + + data["os"] = runtime.GOOS + + // 为了保证程序正常,需要重启 + go self.reload() + } + return renderInstall(ctx, "install/install.html", data) +} + +func (InstallController) SetupOptions(ctx echo.Context) error { + var ( + noEmailConf = false + noQiniuConf = false + ) + + if config.ConfigFile.MustValue("email", "smtp_username") == "" { + noEmailConf = true + } + + if config.ConfigFile.MustValue("qiniu", "access_key") == "" { + noQiniuConf = true + } + + if !noEmailConf && !noQiniuConf { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + if ctx.Request().Method == "POST" { + config.ConfigFile.SetSectionComments("email", "用于注册发送激活码等") + emailFields := []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password", "from_email"} + for _, field := range emailFields { + if field == "smtp_port" && ctx.FormValue("smtp_port") == "" { + config.ConfigFile.SetValue("email", field, "25") + } else { + config.ConfigFile.SetValue("email", field, ctx.FormValue(field)) + } + } + + config.ConfigFile.SetSectionComments("qiniu", "图片存储在七牛云,如果没有可以通过 https://portal.qiniu.com/signup?code=3lfz4at7pxfma 免费申请") + qiniuFields := []string{"access_key", "secret_key", "bucket_name", "http_domain", "https_domain"} + for _, field := range qiniuFields { + config.ConfigFile.SetValue("qiniu", field, ctx.FormValue(field)) + } + if ctx.FormValue("https_domain") == "" { + config.ConfigFile.SetValue("qiniu", "https_domain", ctx.FormValue("http_domain")) + } + + config.SaveConfigFile() + + return renderInstall(ctx, "install/setup-options.html", map[string]interface{}{"success": true}) + } + + data := map[string]interface{}{ + "no_email_conf": noEmailConf, + "no_qiniu_conf": noQiniuConf, + } + return renderInstall(ctx, "install/setup-options.html", data) +} + +func (InstallController) genConfig(ctx echo.Context) error { + env := ctx.FormValue("env") + + config.ConfigFile.SetSectionComments("global", "") + config.ConfigFile.SetValue("global", "env", env) + + var ( + logLevel = "DEBUG" + // domain = global.App.Host + ":" + global.App.Port + xormLogLevel = "0" + xormShowSql = "true" + ) + if env == "pro" { + logLevel = "INFO" + xormLogLevel = "1" + xormShowSql = "false" + } + + config.ConfigFile.SetValue("global", "log_level", logLevel) + config.ConfigFile.SetValue("global", "cookie_secret", goutils.RandString(10)) + config.ConfigFile.SetValue("global", "data_path", "data/max_online_num") + + config.ConfigFile.SetSectionComments("listen", "") + config.ConfigFile.SetValue("listen", "host", "") + config.ConfigFile.SetValue("listen", "port", global.App.Port) + + dbname := ctx.FormValue("dbname") + uname := ctx.FormValue("uname") + pwd := ctx.FormValue("pwd") + dbhost := ctx.FormValue("dbhost") + dbport := ctx.FormValue("dbport") + + config.ConfigFile.SetSectionComments("mysql", "") + config.ConfigFile.SetValue("mysql", "host", dbhost) + config.ConfigFile.SetValue("mysql", "port", dbport) + config.ConfigFile.SetValue("mysql", "user", uname) + config.ConfigFile.SetValue("mysql", "password", pwd) + config.ConfigFile.SetValue("mysql", "dbname", dbname) + config.ConfigFile.SetValue("mysql", "charset", "utf8") + config.ConfigFile.SetKeyComments("mysql", "max_idle", "最大空闲连接数") + config.ConfigFile.SetValue("mysql", "max_idle", "2") + config.ConfigFile.SetKeyComments("mysql", "max_conn", "最大打开连接数") + config.ConfigFile.SetValue("mysql", "max_conn", "10") + + config.ConfigFile.SetSectionComments("xorm", "") + config.ConfigFile.SetValue("xorm", "show_sql", xormShowSql) + config.ConfigFile.SetKeyComments("xorm", "log_level", "0-debug, 1-info, 2-warning, 3-error, 4-off, 5-unknow") + config.ConfigFile.SetValue("xorm", "log_level", xormLogLevel) + + config.ConfigFile.SetSectionComments("security", "") + config.ConfigFile.SetKeyComments("security", "unsubscribe_token_key", "退订邮件使用的 token key") + config.ConfigFile.SetValue("security", "unsubscribe_token_key", goutils.RandString(18)) + config.ConfigFile.SetKeyComments("security", "activate_sign_salt", "注册激活邮件使用的 sign salt") + config.ConfigFile.SetValue("security", "activate_sign_salt", goutils.RandString(18)) + + config.ConfigFile.SetSectionComments("sensitive", "过滤广告") + config.ConfigFile.SetKeyComments("sensitive", "title", "标题关键词") + config.ConfigFile.SetValue("sensitive", "title", "") + config.ConfigFile.SetKeyComments("sensitive", "content", "内容关键词") + config.ConfigFile.SetValue("sensitive", "content", "") + + config.ConfigFile.SetSectionComments("search", "搜索配置") + config.ConfigFile.SetValue("search", "engine_url", "") + + // 校验数据库配置是否正确有效 + if err := db.TestDB(); err != nil { + return err + } + + config.SaveConfigFile() + return nil +} + +func renderInstall(ctx echo.Context, filename string, data map[string]interface{}) error { + objLog := getLogger(ctx) + + if data == nil { + data = make(map[string]interface{}) + } + + filename = config.TemplateDir + filename + + requestURI := ctx.Request().RequestURI + tpl, err := template.ParseFiles(filename) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, data) + if err != nil { + objLog.Errorf("执行模板出错(Execute):[%q] %s\n", requestURI, err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} diff --git a/internal/http/controller/install_unix.go b/internal/http/controller/install_unix.go new file mode 100644 index 00000000..5ed61aba --- /dev/null +++ b/internal/http/controller/install_unix.go @@ -0,0 +1,19 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +//go:build !windows && !plan9 +// +build !windows,!plan9 + +package controller + +import ( + "os" + "syscall" +) + +func (InstallController) reload() { + syscall.Kill(os.Getpid(), syscall.SIGUSR2) +} diff --git a/websites/code/studygolang/src/controller/common.go b/internal/http/controller/install_windows.go similarity index 50% rename from websites/code/studygolang/src/controller/common.go rename to internal/http/controller/install_windows.go index 8c4f814b..4c522906 100644 --- a/websites/code/studygolang/src/controller/common.go +++ b/internal/http/controller/install_windows.go @@ -1,13 +1,10 @@ -// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Copyright 2016 The StudyGolang Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // http://studygolang.com -// Author:polaris studygolang@gmail.com +// Author: polaris polaris@studygolang.com package controller -import ( - "config" -) - -var ROOT = config.ROOT +func (InstallController) reload() { +} diff --git a/internal/http/controller/interview.go b/internal/http/controller/interview.go new file mode 100644 index 00000000..904c4533 --- /dev/null +++ b/internal/http/controller/interview.go @@ -0,0 +1,106 @@ +// Copyright 2022 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strconv" + "time" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeInterview, logic.InterviewComment{}) + logic.RegisterLikeObject(model.TypeInterview, logic.InterviewLike{}) +} + +type InterviewController struct{} + +// RegisterRoute 注册路由 +func (self InterviewController) RegisterRoute(g *echo.Group) { + g.GET("/interview/question", self.TodayQuestion) + g.GET("/interview/question/:show_sn", self.Find) + + g.Match([]string{"GET", "POST"}, "/interview/new", self.Create, middleware.NeedLogin(), middleware.AdminAuth()) +} + +func (InterviewController) Create(ctx echo.Context) error { + question := ctx.FormValue("question") + // 请求新建面试题页面 + if question == "" || ctx.Request().Method != "POST" { + interview := &model.InterviewQuestion{} + return render(ctx, "interview/new.html", map[string]interface{}{"interview": interview}) + } + + forms, _ := ctx.FormParams() + interview, err := logic.DefaultInterview.Publish(context.EchoContext(ctx), forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, interview) +} + +// TodayQuestion 今日题目 +func (ic InterviewController) TodayQuestion(ctx echo.Context) error { + question := logic.DefaultInterview.TodayQuestion(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "title": "Go每日一题 今日(" + time.Now().Format("2006-01-02") + ")", + } + return ic.detail(ctx, question, data) +} + +// Find 某个题目的详情 +func (ic InterviewController) Find(ctx echo.Context) error { + showSn := ctx.Param("show_sn") + sn, err := strconv.ParseInt(showSn, 32, 64) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/interview/question?"+err.Error()) + } + + question, err := logic.DefaultInterview.FindOne(context.EchoContext(ctx), sn) + if err != nil || question.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/interview/question") + } + + data := map[string]interface{}{ + "title": "Go每日一题(" + strconv.Itoa(question.Id) + ")", + } + + return ic.detail(ctx, question, data) +} + +func (InterviewController) detail(ctx echo.Context, question *model.InterviewQuestion, data map[string]interface{}) error { + data["question"] = question + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, question.Id, model.TypeInterview) + // data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, question.Id, model.TypeInterview) + + logic.Views.Incr(Request(ctx), model.TypeInterview, question.Id, me.Uid) + + go logic.DefaultViewRecord.Record(question.Id, model.TypeInterview, me.Uid) + + if me.IsRoot { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), question.Id, model.TypeInterview) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), question.Id, model.TypeInterview) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeInterview, question.Id) + } + + return render(ctx, "interview/question.html,common/comment.html", data) +} diff --git a/internal/http/controller/like.go b/internal/http/controller/like.go new file mode 100644 index 00000000..fab3daa6 --- /dev/null +++ b/internal/http/controller/like.go @@ -0,0 +1,47 @@ +// Copyright 2014 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +// 喜欢系统 + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type LikeController struct{} + +// 注册路由 +func (self LikeController) RegisterRoute(g *echo.Group) { + g.POST("/like/:objid", self.Like, middleware.NeedLogin()) +} + +// Like 喜欢(或取消喜欢) +func (LikeController) Like(ctx echo.Context) error { + form, _ := ctx.FormParams() + if !util.CheckInt(form, "objtype") || !util.CheckInt(form, "flag") { + return fail(ctx, 1, "参数错误") + } + + user := ctx.Get("user").(*model.Me) + objid := goutils.MustInt(ctx.Param("objid")) + objtype := goutils.MustInt(ctx.FormValue("objtype")) + likeFlag := goutils.MustInt(ctx.FormValue("flag")) + + err := logic.DefaultLike.LikeObject(context.EchoContext(ctx), user.Uid, objid, objtype, likeFlag) + if err != nil { + return fail(ctx, 2, "服务器内部错误") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/link.go b/internal/http/controller/link.go new file mode 100644 index 00000000..b92846d6 --- /dev/null +++ b/internal/http/controller/link.go @@ -0,0 +1,29 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" +) + +type LinkController struct{} + +// 注册路由 +func (self LinkController) RegisterRoute(g *echo.Group) { + g.GET("/links", self.FindLinks) +} + +// FindLinks 友情链接 +func (LinkController) FindLinks(ctx echo.Context) error { + + friendLinks := logic.DefaultFriendLink.FindAll(context.EchoContext(ctx)) + + return render(ctx, "link.html", map[string]interface{}{"links": friendLinks}) +} diff --git a/internal/http/controller/message.go b/internal/http/controller/message.go new file mode 100644 index 00000000..3c2588c1 --- /dev/null +++ b/internal/http/controller/message.go @@ -0,0 +1,119 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "fmt" + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type MessageController struct{} + +// 注册路由 +func (self MessageController) RegisterRoute(g *echo.Group) { + messageG := g.Group("/message/", middleware.NeedLogin()) + + messageG.GET(":msgtype", self.ReadList) + messageG.GET("system", self.ReadList) + messageG.Match([]string{"GET", "POST"}, "send", self.Send) + messageG.POST("delete", self.Delete) + + // g.GET("/message/:msgtype", self.ReadList, middleware.NeedLogin()) + // g.GET("/message/system", self.ReadList, middleware.NeedLogin()) + // g.Match([]string{"GET", "POST"}, "/message/send", self.Send, middleware.NeedLogin()) + // g.POST("/message/delete", self.Delete, middleware.NeedLogin()) +} + +// Send 发短消息 +func (MessageController) Send(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + content := ctx.FormValue("content") + // 请求发送消息页面 + if content == "" || ctx.Request().Method != "POST" { + username := ctx.FormValue("username") + if username == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + message := logic.DefaultMessage.FindMsgById(context.EchoContext(ctx), ctx.FormValue("id")) + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + + if message != nil { + if message.To != me.Uid || message.From != user.Uid { + message = nil + } + } + + return render(ctx, "messages/send.html", map[string]interface{}{ + "user": user, + "message": message, + }) + } + + to := goutils.MustInt(ctx.FormValue("to")) + ok := logic.DefaultMessage.SendMessageTo(context.EchoContext(ctx), me.Uid, to, content) + if !ok { + return fail(ctx, 1, "对不起,发送失败,请稍候再试!") + } + + return success(ctx, nil) +} + +// 消息列表 +func (MessageController) ReadList(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + msgtype := ctx.Param("msgtype") + if msgtype == "" { + msgtype = "system" + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + var ( + messages []map[string]interface{} + total int64 + ) + switch msgtype { + case "system": + messages = logic.DefaultMessage.FindSysMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.SysMsgCount(context.EchoContext(ctx), user.Uid) + case "inbox": + messages = logic.DefaultMessage.FindToMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.ToMsgCount(context.EchoContext(ctx), user.Uid) + case "outbox": + messages = logic.DefaultMessage.FindFromMsgsByUid(context.EchoContext(ctx), user.Uid, paginator) + total = logic.DefaultMessage.FromMsgCount(context.EchoContext(ctx), user.Uid) + default: + return ctx.Redirect(http.StatusSeeOther, "/") + } + + pageHtml := paginator.SetTotal(total).GetPageHtml(fmt.Sprintf("/message/%s", msgtype)) + + return render(ctx, "messages/list.html", map[string]interface{}{"messages": messages, "msgtype": msgtype, "page": template.HTML(pageHtml)}) +} + +// 删除消息 +func (MessageController) Delete(ctx echo.Context) error { + id := ctx.FormValue("id") + msgtype := ctx.FormValue("msgtype") + if !logic.DefaultMessage.DeleteMessage(context.EchoContext(ctx), id, msgtype) { + return fail(ctx, 1, "对不起,删除失败,请稍候再试!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/mission.go b/internal/http/controller/mission.go new file mode 100644 index 00000000..19f30fd4 --- /dev/null +++ b/internal/http/controller/mission.go @@ -0,0 +1,64 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/times" +) + +type MissionController struct{} + +// 注册路由 +func (self MissionController) RegisterRoute(g *echo.Group) { + g.GET("/mission/daily", self.Daily, middleware.NeedLogin()) + g.GET("/mission/daily/redeem", self.DailyRedeem, middleware.NeedLogin()) + g.GET("/mission/complete/:id", self.Complete, middleware.NeedLogin()) +} + +func (MissionController) Daily(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + userLoginMission := logic.DefaultMission.FindLoginMission(context.EchoContext(ctx), me) + userLoginMission.Uid = me.Uid + + data := map[string]interface{}{"login_mission": userLoginMission} + + if userLoginMission != nil && times.Format("Ymd") == strconv.Itoa(userLoginMission.Date) { + data["had_redeem"] = true + } else { + data["had_redeem"] = false + } + + fr := ctx.QueryParam("fr") + if fr == "redeem" { + data["show_msg"] = true + } + return render(ctx, "mission/daily.html", data) +} + +func (MissionController) DailyRedeem(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + logic.DefaultMission.RedeemLoginAward(context.EchoContext(ctx), me) + + return ctx.Redirect(http.StatusSeeOther, "/mission/daily?fr=redeem") +} + +func (MissionController) Complete(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + id := ctx.Param("id") + logic.DefaultMission.Complete(context.EchoContext(ctx), me, id) + + return ctx.Redirect(http.StatusSeeOther, "/balance") +} diff --git a/internal/http/controller/oauth.go b/internal/http/controller/oauth.go new file mode 100644 index 00000000..7a073fe9 --- /dev/null +++ b/internal/http/controller/oauth.go @@ -0,0 +1,115 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type OAuthController struct{} + +// 注册路由 +func (self OAuthController) RegisterRoute(g *echo.Group) { + g.GET("/oauth/github/callback", self.GithubCallback) + g.GET("/oauth/github/login", self.GithubLogin) + + g.GET("/oauth/gitea/callback", self.GiteaCallback) + g.GET("/oauth/gitea/login", self.GiteaLogin) +} + +func (OAuthController) GithubLogin(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + url := logic.DefaultThirdUser.GithubAuthCodeUrl(context.EchoContext(ctx), uri) + return ctx.Redirect(http.StatusSeeOther, url) +} + +func (OAuthController) GithubCallback(ctx echo.Context) error { + code := ctx.FormValue("code") + + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 已登录用户,绑定 github + logic.DefaultThirdUser.BindGithub(context.EchoContext(ctx), code, me) + + redirectURL := ctx.QueryParam("redirect_url") + if redirectURL == "" { + redirectURL = "/account/edit#connection" + } + return ctx.Redirect(http.StatusSeeOther, redirectURL) + } + + user, err := logic.DefaultThirdUser.LoginFromGithub(context.EchoContext(ctx), code) + if err != nil || user.Uid == 0 { + var errMsg = "" + if err != nil { + errMsg = err.Error() + } else { + errMsg = "服务内部错误" + } + + return render(ctx, "login.html", map[string]interface{}{"error": errMsg}) + } + + // 登录成功,种cookie + SetLoginCookie(ctx, user.Username) + + if user.Balance == 0 { + return ctx.Redirect(http.StatusSeeOther, "/balance") + } + + return ctx.Redirect(http.StatusSeeOther, "/") +} + +func (OAuthController) GiteaLogin(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + url := logic.DefaultThirdUser.GiteaAuthCodeUrl(context.EchoContext(ctx), uri) + return ctx.Redirect(http.StatusSeeOther, url) +} + +func (OAuthController) GiteaCallback(ctx echo.Context) error { + code := ctx.FormValue("code") + + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 已登录用户,绑定 github + logic.DefaultThirdUser.BindGitea(context.EchoContext(ctx), code, me) + + redirectURL := ctx.QueryParam("redirect_url") + if redirectURL == "" { + redirectURL = "/account/edit#connection" + } + return ctx.Redirect(http.StatusSeeOther, redirectURL) + } + + user, err := logic.DefaultThirdUser.LoginFromGitea(context.EchoContext(ctx), code) + if err != nil || user.Uid == 0 { + var errMsg = "" + if err != nil { + errMsg = err.Error() + } else { + errMsg = "服务内部错误" + } + + return render(ctx, "login.html", map[string]interface{}{"error": errMsg}) + } + + // 登录成功,种cookie + SetLoginCookie(ctx, user.Username) + + if user.Balance == 0 { + return ctx.Redirect(http.StatusSeeOther, "/balance") + } + + return ctx.Redirect(http.StatusSeeOther, "/") +} diff --git a/internal/http/controller/other.go b/internal/http/controller/other.go new file mode 100644 index 00000000..fea59eaa --- /dev/null +++ b/internal/http/controller/other.go @@ -0,0 +1,36 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// https://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "path" + + "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + + "github.com/studygolang/studygolang/util" +) + +// OtherController 有些页面只是前端,因此通过这个页面统一控制 +// 只需要创建模板文件就可以访问到 +type OtherController struct{} + +// RegisterRoute 注册路由 +func (self OtherController) RegisterRoute(g *echo.Group) { + g.GET("/*", self.Any) +} + +func (OtherController) Any(ctx echo.Context) error { + uri := ctx.Request().RequestURI + tplFile := uri + ".html" + if util.Exist(path.Clean(config.TemplateDir + tplFile)) { + return render(ctx, tplFile, nil) + } + + return echo.NewHTTPError(http.StatusNotFound) +} diff --git a/internal/http/controller/project.go b/internal/http/controller/project.go new file mode 100644 index 00000000..18a90860 --- /dev/null +++ b/internal/http/controller/project.go @@ -0,0 +1,169 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeProject, logic.ProjectComment{}) + logic.RegisterLikeObject(model.TypeProject, logic.ProjectLike{}) +} + +type ProjectController struct{} + +// 注册路由 +func (self ProjectController) RegisterRoute(g *echo.Group) { + g.GET("/projects", self.ReadList) + g.Match([]string{"GET", "POST"}, "/project/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/project/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + g.GET("/p/:uri", self.Detail) + g.GET("/project/uri", self.CheckExist) +} + +// ReadList 开源项目列表页 +func (ProjectController) ReadList(ctx echo.Context) error { + limit := 20 + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + paginator.SetPerPage(limit) + total := logic.DefaultProject.Count(context.EchoContext(ctx), "") + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + pageInfo := template.HTML(pageHtml) + + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", "status IN(?,?)", model.ProjectStatusNew, model.ProjectStatusOnline) + + num := len(projects) + if num == 0 { + return render(ctx, "projects/list.html", map[string]interface{}{"projects": projects, "activeProjects": "active"}) + } + + // 获取当前用户喜欢对象信息 + me, ok := ctx.Get("user").(*model.Me) + var likeFlags map[int]int + if ok { + likeFlags, _ = logic.DefaultLike.FindUserLikeObjects(context.EchoContext(ctx), me.Uid, model.TypeProject, projects[0].Id, projects[num-1].Id) + } + + return render(ctx, "projects/list.html", map[string]interface{}{"projects": projects, "activeProjects": "active", "page": pageInfo, "likeflags": likeFlags}) +} + +// Create 新建项目 +func (ProjectController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + name := ctx.FormValue("name") + // 请求新建项目页面 + if name == "" || ctx.Request().Method != "POST" { + project := &model.OpenProject{} + + data := map[string]interface{}{"project": project, "activeProjects": "active"} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + + return render(ctx, "projects/new.html", data) + } + + forms, _ := ctx.FormParams() + err := logic.DefaultProject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Modify 修改项目 +func (ProjectController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/projects") + } + + // 请求编辑项目页面 + if ctx.Request().Method != "POST" { + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), id) + return render(ctx, "projects/new.html", map[string]interface{}{"project": project, "activeProjects": "active"}) + } + + user := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultProject.Publish(context.EchoContext(ctx), user, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return ctx.String(http.StatusForbidden, "没有权限") + } + return fail(ctx, 1, "内部服务错误!") + } + return success(ctx, nil) +} + +// Detail 项目详情 +func (ProjectController) Detail(ctx echo.Context) error { + project := logic.DefaultProject.FindOne(context.EchoContext(ctx), ctx.Param("uri")) + if project == nil || project.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/projects") + } + + data := map[string]interface{}{ + "activeProjects": "active", + "project": project, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, project.Id, model.TypeProject) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, project.Id, model.TypeProject) + + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id, me.Uid) + + if me.Uid != project.User.Uid { + go logic.DefaultViewRecord.Record(project.Id, model.TypeProject, me.Uid) + } + + if me.IsRoot || me.Uid == project.User.Uid { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), project.Id, model.TypeProject) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), project.Id, model.TypeProject) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeProject, project.Id) + } + + // 为了阅读数即时看到 + project.Viewnum++ + + return render(ctx, "projects/detail.html,common/comment.html", data) +} + +// CheckExist 检测 uri 对应的项目是否存在(验证,true表示不存在;false表示存在) +func (ProjectController) CheckExist(ctx echo.Context) error { + uri := ctx.QueryParam("uri") + if uri == "" { + return ctx.JSON(http.StatusOK, `true`) + } + + if logic.DefaultProject.UriExists(context.EchoContext(ctx), uri) { + return ctx.JSON(http.StatusOK, `false`) + } + return ctx.JSON(http.StatusOK, `true`) +} diff --git a/internal/http/controller/reading.go b/internal/http/controller/reading.go new file mode 100644 index 00000000..754a6f6f --- /dev/null +++ b/internal/http/controller/reading.go @@ -0,0 +1,82 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type ReadingController struct{} + +// 注册路由 +func (self ReadingController) RegisterRoute(g *echo.Group) { + g.GET("/readings", self.ReadingList) + g.GET("/readings/:id", self.IReading) +} + +// ReadingList 晨读列表页 +func (ReadingController) ReadingList(ctx echo.Context) error { + limit := 20 + lastId := goutils.MustInt(ctx.QueryParam("lastid")) + rtype := goutils.MustInt(ctx.QueryParam("rtype"), model.RtypeGo) + + readings := logic.DefaultReading.FindBy(context.EchoContext(ctx), limit+5, rtype, lastId) + num := len(readings) + if num == 0 { + if lastId == 0 { + return render(ctx, "readings/list.html", map[string]interface{}{"activeReadings": "active", "readings": readings, "rtype": rtype}) + } else { + return ctx.Redirect(http.StatusSeeOther, "/readings") + } + } + + var ( + hasPrev, hasNext bool + prevId, nextId int + ) + + if lastId > 0 { + prevId = lastId + + // 避免因为项目下线,导致判断错误(所以 > 5) + if prevId-readings[0].Id > 5 { + hasPrev = false + } else { + prevId += limit + hasPrev = true + } + } + + if num > limit { + hasNext = true + readings = readings[:limit] + nextId = readings[limit-1].Id + } else { + nextId = readings[num-1].Id + } + + pageInfo := map[string]interface{}{ + "has_prev": hasPrev, + "prev_id": prevId, + "has_next": hasNext, + "next_id": nextId, + } + return render(ctx, "readings/list.html", map[string]interface{}{"activeReadings": "active", "readings": readings, "page": pageInfo, "rtype": rtype}) +} + +// IReading 点击 【我要晨读】,记录点击数,跳转 +func (ReadingController) IReading(ctx echo.Context) error { + uri := logic.DefaultReading.IReading(context.EchoContext(ctx), goutils.MustInt(ctx.Param("id"))) + return ctx.Redirect(http.StatusSeeOther, uri) +} diff --git a/internal/http/controller/resource.go b/internal/http/controller/resource.go new file mode 100644 index 00000000..857e845c --- /dev/null +++ b/internal/http/controller/resource.go @@ -0,0 +1,162 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeResource, logic.ResourceComment{}) + logic.RegisterLikeObject(model.TypeResource, logic.ResourceLike{}) +} + +type ResourceController struct{} + +// 注册路由 +func (self ResourceController) RegisterRoute(g *echo.Group) { + g.GET("/resources", self.ReadList) + g.GET("/resources/cat/:catid", self.ReadCatResources) + g.GET("/resources/:id", self.Detail) + g.Match([]string{"GET", "POST"}, "/resources/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/resources/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +// ReadList 资源索引页 +func (ResourceController) ReadList(ctx echo.Context) error { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") +} + +// ReadCatResources 某个分类的资源列表 +func (ResourceController) ReadCatResources(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + catid := goutils.MustInt(ctx.Param("catid")) + + resources, total := logic.DefaultResource.FindByCatid(context.EchoContext(ctx), paginator, catid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "resources/index.html", map[string]interface{}{"activeResources": "active", "resources": resources, "categories": logic.AllCategory, "page": template.HTML(pageHtml), "curCatid": catid}) +} + +// Detail 某个资源详细页 +func (ResourceController) Detail(ctx echo.Context) error { + id := goutils.MustInt(ctx.Param("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + resource, comments := logic.DefaultResource.FindById(context.EchoContext(ctx), id) + if len(resource) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + + data := map[string]interface{}{ + "activeResources": "active", + "resource": resource, + "comments": comments, + } + + me, ok := ctx.Get("user").(*model.Me) + if ok { + id := resource["id"].(int) + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, id, model.TypeResource) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, id, model.TypeResource) + + logic.Views.Incr(Request(ctx), model.TypeResource, id, me.Uid) + + if me.Uid != resource["uid"].(int) { + go logic.DefaultViewRecord.Record(id, model.TypeResource, me.Uid) + } + + if me.IsRoot || me.Uid == resource["uid"].(int) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), id, model.TypeResource) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), id, model.TypeResource) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeResource, id) + } + + return render(ctx, "resources/detail.html,common/comment.html", data) +} + +// Create 发布新资源 +func (ResourceController) Create(ctx echo.Context) error { + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + // 请求新建资源页面 + if title == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{"activeResources": "active", "categories": logic.AllCategory} + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + return render(ctx, "resources/new.html", data) + } + + errMsg := "" + resForm := ctx.FormValue("form") + if resForm == model.LinkForm { + if ctx.FormValue("url") == "" { + errMsg = "url不能为空" + } + } else { + if ctx.FormValue("content") == "" { + errMsg = "内容不能为空" + } + } + if errMsg != "" { + return fail(ctx, 1, errMsg) + } + + forms, _ := ctx.FormParams() + err := logic.DefaultResource.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "内部服务错误,请稍候再试!") + } + + return success(ctx, nil) +} + +// Modify 修改資源 +func (ResourceController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/resources/cat/1") + } + + // 请求编辑資源页面 + if ctx.Request().Method != "POST" { + resource := logic.DefaultResource.FindResource(context.EchoContext(ctx), id) + return render(ctx, "resources/new.html", map[string]interface{}{"resource": resource, "activeResources": "active", "categories": logic.AllCategory}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultResource.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return ctx.String(http.StatusForbidden, "没有权限修改") + } + return fail(ctx, 2, "内部服务错误,请稍候再试!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/routes.go b/internal/http/controller/routes.go new file mode 100644 index 00000000..f764698a --- /dev/null +++ b/internal/http/controller/routes.go @@ -0,0 +1,49 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import echo "github.com/labstack/echo/v4" + +func RegisterRoutes(g *echo.Group) { + new(IndexController).RegisterRoute(g) + new(AccountController).RegisterRoute(g) + new(TopicController).RegisterRoute(g) + new(ArticleController).RegisterRoute(g) + new(ProjectController).RegisterRoute(g) + new(ResourceController).RegisterRoute(g) + new(ReadingController).RegisterRoute(g) + new(WikiController).RegisterRoute(g) + new(UserController).RegisterRoute(g) + new(LikeController).RegisterRoute(g) + new(FavoriteController).RegisterRoute(g) + new(MessageController).RegisterRoute(g) + new(SidebarController).RegisterRoute(g) + new(CommentController).RegisterRoute(g) + new(SearchController).RegisterRoute(g) + new(WideController).RegisterRoute(g) + new(ImageController).RegisterRoute(g) + new(CaptchaController).RegisterRoute(g) + new(BookController).RegisterRoute(g) + new(MissionController).RegisterRoute(g) + new(UserRichController).RegisterRoute(g) + new(TopController).RegisterRoute(g) + new(GiftController).RegisterRoute(g) + new(OAuthController).RegisterRoute(g) + new(WebsocketController).RegisterRoute(g) + new(DownloadController).RegisterRoute(g) + new(LinkController).RegisterRoute(g) + new(SubjectController).RegisterRoute(g) + new(GCTTController).RegisterRoute(g) + new(FeedController).RegisterRoute(g) + new(InterviewController).RegisterRoute(g) + + new(WechatController).RegisterRoute(g) + + new(InstallController).RegisterRoute(g) + + new(OtherController).RegisterRoute(g) +} diff --git a/internal/http/controller/search.go b/internal/http/controller/search.go new file mode 100644 index 00000000..d72768dc --- /dev/null +++ b/internal/http/controller/search.go @@ -0,0 +1,87 @@ +package controller + +import ( + "html" + "net/http" + "net/url" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type SearchController struct{} + +// 注册路由 +func (self SearchController) RegisterRoute(g *echo.Group) { + g.GET("/search", self.Search) + g.GET("/tag/:name", self.TagList) +} + +// Search +func (SearchController) Search(ctx echo.Context) error { + q := ctx.QueryParam("q") + field := ctx.QueryParam("f") + p := goutils.MustInt(ctx.QueryParam("p"), 1) + + rows := 50 + + respBody, err := logic.DefaultSearcher.DoSearch(q, field, (p-1)*rows, rows) + + data := map[string]interface{}{ + "respBody": respBody, + "q": q, + "f": field, + } + if err != nil { + return render(ctx, "500.html", nil) + } + uri := "/search?q=" + html.EscapeString(q) + "&f=" + field + "&" + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(int64(respBody.NumFound)).GetPageHtml(uri) + + return render(ctx, "search.html", data) +} + +// TagList +func (SearchController) TagList(ctx echo.Context) error { + field := "tag" + p := goutils.MustInt(ctx.QueryParam("p"), 1) + q := ctx.Param("name") + if q == "" { + return render(ctx, "notfound", nil) + } + + var err error + q, err = url.QueryUnescape(q) + if err != nil { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 过滤非法 tag + if len(q) > 9 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + rows := 50 + + respBody, err := logic.DefaultSearcher.DoSearch(q, field, (p-1)*rows, rows) + users, nodes := logic.DefaultSearcher.FillNodeAndUser(context.EchoContext(ctx), respBody) + + data := map[string]interface{}{ + "respBody": respBody, + "name": q, + "users": users, + "nodes": nodes, + } + if err != nil { + return render(ctx, "500.html", nil) + } + uri := "/tag/" + q + "?" + paginator := logic.NewPaginatorWithPerPage(p, rows) + data["pageHtml"] = paginator.SetTotal(int64(respBody.NumFound)).GetPageHtml(uri) + + return render(ctx, "feed/tag.html", data) +} diff --git a/internal/http/controller/sidebar.go b/internal/http/controller/sidebar.go new file mode 100644 index 00000000..9e30552e --- /dev/null +++ b/internal/http/controller/sidebar.go @@ -0,0 +1,191 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "strconv" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" + "github.com/polaris1119/times" +) + +// 侧边栏的内容通过异步请求获取 +type SidebarController struct{} + +func (self SidebarController) RegisterRoute(g *echo.Group) { + g.GET("/readings/recent", self.RecentReading) + g.GET("/topics/:nid/others", self.OtherTopics) + g.GET("/websites/stat", self.WebsiteStat) + g.GET("/dynamics/recent", self.RecentDynamic) + g.GET("/topics/recent", self.RecentTopic) + g.GET("/articles/recent", self.RecentArticle) + g.GET("/projects/recent", self.RecentProject) + g.GET("/resources/recent", self.RecentResource) + g.GET("/comments/recent", self.RecentComment) + g.GET("/nodes/hot", self.HotNodes) + g.GET("/users/active", self.ActiveUser) + g.GET("/users/newest", self.NewestUser) + g.GET("/friend/links", self.FriendLinks) + g.GET("/rank/view", self.ViewRank) +} + +// RecentReading 技术晨读 +func (SidebarController) RecentReading(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 7) + readings := logic.DefaultReading.FindBy(context.EchoContext(ctx), limit, model.RtypeGo) + if len(readings) == 1 { + // 首页,三天内的晨读才显示 + if time.Time(readings[0].Ctime).Before(time.Now().Add(-3 * 24 * time.Hour)) { + readings = nil + } + } + return success(ctx, readings) +} + +// OtherTopics 某节点下其他帖子 +func (SidebarController) OtherTopics(ctx echo.Context) error { + topics := logic.DefaultTopic.FindByNid(context.EchoContext(ctx), ctx.Param("nid"), ctx.QueryParam("tid")) + topics = logic.DefaultTopic.JSEscape(topics) + return success(ctx, topics) +} + +// WebsiteStat 网站统计信息 +func (SidebarController) WebsiteStat(ctx echo.Context) error { + articleTotal := logic.DefaultArticle.Total() + projectTotal := logic.DefaultProject.Total() + topicTotal := logic.DefaultTopic.Total() + cmtTotal := logic.DefaultComment.Total() + resourceTotal := logic.DefaultResource.Total() + bookTotal := logic.DefaultGoBook.Total() + userTotal := logic.DefaultUser.Total() + + data := map[string]interface{}{ + "article": articleTotal, + "project": projectTotal, + "topic": topicTotal, + "resource": resourceTotal, + "book": bookTotal, + "comment": cmtTotal, + "user": userTotal, + } + + return success(ctx, data) +} + +// RecentDynamic 社区最新公告或go最新动态 +func (SidebarController) RecentDynamic(ctx echo.Context) error { + dynamics := logic.DefaultDynamic.FindBy(context.EchoContext(ctx), 0, 3) + return success(ctx, dynamics) +} + +// RecentTopic 最新帖子 +func (SidebarController) RecentTopic(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + topicList := logic.DefaultTopic.FindRecent(limit) + return success(ctx, topicList) +} + +// RecentArticle 最新博文 +func (SidebarController) RecentArticle(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentArticles := logic.DefaultArticle.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentArticles) +} + +// RecentProject 最新开源项目 +func (SidebarController) RecentProject(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentProjects := logic.DefaultProject.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentProjects) +} + +// RecentResource 最新资源 +func (SidebarController) RecentResource(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentResources := logic.DefaultResource.FindBy(context.EchoContext(ctx), limit) + return success(ctx, recentResources) +} + +// RecentComment 最新评论 +func (SidebarController) RecentComment(ctx echo.Context) error { + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + recentComments := logic.DefaultComment.FindRecent(context.EchoContext(ctx), 0, -1, limit) + + uids := slices.StructsIntSlice(recentComments, "Uid") + users := logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + + result := map[string]interface{}{ + "comments": recentComments, + } + + // json encode 不支持 map[int]... + for uid, user := range users { + result[strconv.Itoa(uid)] = user + } + + return success(ctx, result) +} + +// HotNodes 社区热门节点 +func (SidebarController) HotNodes(ctx echo.Context) error { + nodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + return success(ctx, nodes) +} + +// ActiveUser 活跃会员 +func (SidebarController) ActiveUser(ctx echo.Context) error { + // activeUsers := logic.DefaultUser.FindActiveUsers(ctx, 9) + // return success(ctx, activeUsers) + activeUsers := logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 9) + return success(ctx, activeUsers) +} + +// NewestUser 新加入会员 +func (SidebarController) NewestUser(ctx echo.Context) error { + newestUsers := logic.DefaultUser.FindNewUsers(context.EchoContext(ctx), 9) + return success(ctx, newestUsers) +} + +// FriendLinks 友情链接 +func (SidebarController) FriendLinks(ctx echo.Context) error { + friendLinks := logic.DefaultFriendLink.FindAll(context.EchoContext(ctx), 10) + return success(ctx, friendLinks) +} + +// ViewRank 阅读排行榜 +func (SidebarController) ViewRank(ctx echo.Context) error { + objtype := goutils.MustInt(ctx.QueryParam("objtype")) + rankType := ctx.QueryParam("rank_type") + limit := goutils.MustInt(ctx.QueryParam("limit"), 10) + + var result = map[string]interface{}{ + "objtype": objtype, + "rank_type": rankType, + } + switch rankType { + case "today": + result["list"] = logic.DefaultRank.FindDayRank(context.EchoContext(ctx), objtype, times.Format("ymd"), limit) + case "yesterday": + yesterday := time.Now().Add(-1 * 24 * time.Hour) + result["list"] = logic.DefaultRank.FindDayRank(context.EchoContext(ctx), objtype, times.Format("ymd", yesterday), limit) + case "week": + result["list"] = logic.DefaultRank.FindWeekRank(context.EchoContext(ctx), objtype, limit) + case "month": + result["list"] = logic.DefaultRank.FindMonthRank(context.EchoContext(ctx), objtype, limit) + } + + result["path"] = model.PathUrlMap[objtype] + + return success(ctx, result) +} diff --git a/internal/http/controller/subject.go b/internal/http/controller/subject.go new file mode 100644 index 00000000..60b40bc8 --- /dev/null +++ b/internal/http/controller/subject.go @@ -0,0 +1,207 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +type SubjectController struct{} + +// 注册路由 +func (self SubjectController) RegisterRoute(g *echo.Group) { + g.GET("/subject/:id", self.Index) + g.POST("/subject/follow", self.Follow, middleware.NeedLogin()) + g.GET("/subject/my_articles", self.MyArticles, middleware.NeedLogin()) + g.POST("/subject/contribute", self.Contribute, middleware.NeedLogin()) + g.POST("/subject/remove_contribute", self.RemoveContribute, middleware.NeedLogin()) + g.GET("/subject/mine", self.Mine, middleware.NeedLogin()) + + g.Match([]string{"GET", "POST"}, "/subject/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice()) + g.Match([]string{"GET", "POST"}, "/subject/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) +} + +func (SubjectController) Index(ctx echo.Context) error { + id := goutils.MustInt(ctx.Param("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + subject := logic.DefaultSubject.FindOne(context.EchoContext(ctx), id) + if subject.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + if subject.Cover != "" && !strings.HasPrefix(subject.Cover, "http") { + cdnDomain := global.App.CanonicalCDN(CheckIsHttps(ctx)) + subject.Cover = cdnDomain + subject.Cover + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + orderBy := ctx.QueryParam("order_by") + articles := logic.DefaultSubject.FindArticles(context.EchoContext(ctx), id, paginator, orderBy) + if orderBy == "" { + orderBy = "added_at" + } + + articleNum := logic.DefaultSubject.FindArticleTotal(context.EchoContext(ctx), id) + + pageHtml := paginator.SetTotal(articleNum).GetPageHtml(ctx.Request().URL.Path) + + followers := logic.DefaultSubject.FindFollowers(context.EchoContext(ctx), id) + followerNum := logic.DefaultSubject.FindFollowerTotal(context.EchoContext(ctx), id) + + // 是否已关注 + followed := false + me, ok := ctx.Get("user").(*model.Me) + if ok { + followed = logic.DefaultSubject.HadFollow(context.EchoContext(ctx), id, me) + } + + data := map[string]interface{}{ + "subject": subject, + "articles": articles, + "article_num": articleNum, + "followers": followers, + "follower_num": followerNum, + "order_by": orderBy, + "followed": followed, + "page": pageHtml, + } + + return render(ctx, "subject/index.html", data) +} + +func (self SubjectController) Follow(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + + me := ctx.Get("user").(*model.Me) + err := logic.DefaultSubject.Follow(context.EchoContext(ctx), sid, me) + if err != nil { + return fail(ctx, 1, "关注失败!") + } + + return success(ctx, nil) +} + +func (self SubjectController) MyArticles(ctx echo.Context) error { + kw := ctx.QueryParam("kw") + sid := goutils.MustInt(ctx.FormValue("sid")) + + me := ctx.Get("user").(*model.Me) + + articles := logic.DefaultArticle.SearchMyArticles(context.EchoContext(ctx), me, sid, kw) + + return success(ctx, map[string]interface{}{ + "articles": articles, + }) +} + +// Contribute 投稿 +func (self SubjectController) Contribute(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + articleId := goutils.MustInt(ctx.FormValue("article_id")) + + me := ctx.Get("user").(*model.Me) + + err := logic.DefaultSubject.Contribute(context.EchoContext(ctx), me, sid, articleId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// RemoveContribute 删除投稿 +func (self SubjectController) RemoveContribute(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + articleId := goutils.MustInt(ctx.FormValue("article_id")) + + err := logic.DefaultSubject.RemoveContribute(context.EchoContext(ctx), sid, articleId) + if err != nil { + return fail(ctx, 1, err.Error()) + } + + return success(ctx, nil) +} + +// Mine 我管理的专栏 +func (self SubjectController) Mine(ctx echo.Context) error { + kw := ctx.QueryParam("kw") + articleId := goutils.MustInt(ctx.FormValue("article_id")) + me := ctx.Get("user").(*model.Me) + + subjects := logic.DefaultSubject.FindMine(context.EchoContext(ctx), me, articleId, kw) + + return success(ctx, map[string]interface{}{"subjects": subjects}) +} + +// Create 新建专栏 +func (SubjectController) Create(ctx echo.Context) error { + + name := ctx.FormValue("name") + // 请求新建专栏页面 + if name == "" || ctx.Request().Method != "POST" { + data := map[string]interface{}{} + return render(ctx, "subject/new.html", data) + } + + exist := logic.DefaultSubject.ExistByName(name) + if exist { + return fail(ctx, 1, "专栏已经存在 : "+name) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + sid, err := logic.DefaultSubject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误:"+err.Error()) + } + + return success(ctx, map[string]interface{}{"sid": sid}) +} + +// Modify 修改专栏 +func (SubjectController) Modify(ctx echo.Context) error { + sid := goutils.MustInt(ctx.FormValue("sid")) + if sid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/subjects") + } + + if ctx.Request().Method != "POST" { + subject := logic.DefaultSubject.FindOne(context.EchoContext(ctx), sid) + if subject == nil { + return ctx.Redirect(http.StatusSeeOther, "/subjects") + } + + data := map[string]interface{}{ + "subject": subject, + } + + return render(ctx, "subject/new.html", data) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultSubject.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 2, "服务错误,请稍后重试!") + } + return success(ctx, map[string]interface{}{"sid": sid}) +} diff --git a/internal/http/controller/top.go b/internal/http/controller/top.go new file mode 100644 index 00000000..469615a0 --- /dev/null +++ b/internal/http/controller/top.go @@ -0,0 +1,42 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/times" +) + +type TopController struct{} + +// 注册路由 +func (self TopController) RegisterRoute(g *echo.Group) { + g.GET("/top/dau", self.TopDAU) + g.GET("/top/rich", self.TopRich) +} + +func (TopController) TopDAU(ctx echo.Context) error { + data := map[string]interface{}{ + "today": times.Format("Ymd"), + } + + data["users"] = logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 10) + data["active_num"] = logic.DefaultRank.TotalDAUUser(context.EchoContext(ctx)) + + return render(ctx, "top/dau.html", data) +} + +func (TopController) TopRich(ctx echo.Context) error { + data := map[string]interface{}{ + "users": logic.DefaultRank.FindRichRank(context.EchoContext(ctx)), + } + + return render(ctx, "top/rich.html", data) +} diff --git a/internal/http/controller/topic.go b/internal/http/controller/topic.go new file mode 100644 index 00000000..4c8897dc --- /dev/null +++ b/internal/http/controller/topic.go @@ -0,0 +1,365 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + "strconv" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + logic.RegisterCommentObject(model.TypeTopic, logic.TopicComment{}) + logic.RegisterLikeObject(model.TypeTopic, logic.TopicLike{}) +} + +type TopicController struct{} + +// 注册路由 +func (self TopicController) RegisterRoute(g *echo.Group) { + g.GET("/topics", self.TopicList) + g.GET("/topics/no_reply", self.TopicsNoReply) + g.GET("/topics/last", self.TopicsLast) + g.GET("/topics/:tid", self.Detail) + g.GET("/topics/node/:nid", self.NodeTopics) + g.GET("/go/:node", self.GoNodeTopics) + g.GET("/nodes", self.Nodes) + + g.Match([]string{"GET", "POST"}, "/topics/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck(), middleware.PublishNotice(), middleware.CheckCaptcha()) + g.Match([]string{"GET", "POST"}, "/topics/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + + g.POST("/topics/set_top", self.SetTop, middleware.NeedLogin()) + + g.Match([]string{"GET", "POST"}, "/append/topic/:tid", self.Append, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck()) +} + +func (self TopicController) TopicList(ctx echo.Context) error { + tab := ctx.QueryParam("tab") + if tab == "" { + tab = GetFromCookie(ctx, "TOPIC_TAB") + } else { + SetCookie(ctx, "TOPIC_TAB", tab) + } + + if tab != "" && tab != "all" { + nid := logic.GetNidByEname(tab) + if nid > 0 { + return self.topicList(ctx, tab, "topics.mtime DESC", "nid=? AND top!=1", nid) + } + } + + return self.topicList(ctx, "all", "topics.mtime DESC", "top!=1") +} + +func (self TopicController) Topics(ctx echo.Context) error { + return self.topicList(ctx, "", "topics.mtime DESC", "") +} + +func (self TopicController) TopicsNoReply(ctx echo.Context) error { + return self.topicList(ctx, "no_reply", "topics.mtime DESC", "lastreplyuid=?", 0) +} + +func (self TopicController) TopicsLast(ctx echo.Context) error { + return self.topicList(ctx, "last", "ctime DESC", "") +} + +func (TopicController) topicList(ctx echo.Context, tab, orderBy, querystring string, args ...interface{}) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + // 置顶的topic + topTopics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "ctime DESC", "top=1") + + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, orderBy, querystring, args...) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, args...) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topics": append(topTopics, topics...), + "activeTopics": "active", + "nodes": logic.GenNodes(), + "tab": tab, + "tab_list": hotNodes, + "page": template.HTML(pageHtml), + } + + return render(ctx, "topics/list.html", data) +} + +// NodeTopics 某节点下的主题列表 +func (TopicController) NodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring, nid := "nid=?", goutils.MustInt(ctx.Param("nid")) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + // 当前节点信息 + node := logic.GetNode(nid) + + return render(ctx, "topics/node.html", map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// GoNodeTopics 某节点下的主题列表,uri: /go/golang +func (TopicController) GoNodeTopics(ctx echo.Context) error { + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + ename := ctx.Param("node") + node := logic.GetNodeByEname(ename) + if node == nil { + return render(ctx, "notfound.html", nil) + } + + querystring, nid := "nid=?", node["nid"].(int) + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.mtime DESC", querystring, nid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, nid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "topics/node.html", map[string]interface{}{"activeTopics": "active", "topics": topics, "page": template.HTML(pageHtml), "total": total, "node": node}) +} + +// Detail 社区主题详细页 +func (TopicController) Detail(ctx echo.Context) error { + tid := goutils.MustInt(ctx.Param("tid")) + if tid == 0 { + return render(ctx, "notfound.html", nil) + } + + topic, replies, err := logic.DefaultTopic.FindByTid(context.EchoContext(ctx), tid) + if err != nil { + return render(ctx, "notfound.html", nil) + } + + data := map[string]interface{}{ + "activeTopics": "active", + "topic": topic, + "replies": replies, + "appends": []*model.TopicAppend{}, + "can_view": true, + } + + me, ok := ctx.Get("user").(*model.Me) + if topic["permission"] == model.PermissionOnlyMe { + if !ok || (topic["uid"].(int) != me.Uid && !me.IsRoot) { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + } else if topic["permission"] == model.PermissionPay { + // 当前用户是否对付费内容可见 + if !ok || (!me.IsVip && !me.IsRoot && topic["uid"].(int) != me.Uid) { + data["can_view"] = false + } + } + + if topic["permission"] == model.PermissionPublic || + (topic["permission"] == model.PermissionLogin && ok) || + (topic["permission"] == model.PermissionPay && ok && (me.IsVip || me.IsRoot)) { + data["appends"] = logic.DefaultTopic.FindAppend(context.EchoContext(ctx), tid) + } + + if ok { + tid := topic["tid"].(int) + data["likeflag"] = logic.DefaultLike.HadLike(context.EchoContext(ctx), me.Uid, tid, model.TypeTopic) + data["hadcollect"] = logic.DefaultFavorite.HadFavorite(context.EchoContext(ctx), me.Uid, tid, model.TypeTopic) + + logic.Views.Incr(Request(ctx), model.TypeTopic, tid, me.Uid) + + if me.Uid != topic["uid"].(int) { + go logic.DefaultViewRecord.Record(tid, model.TypeTopic, me.Uid) + } + + if me.IsRoot || me.Uid == topic["uid"].(int) { + data["view_user_num"] = logic.DefaultViewRecord.FindUserNum(context.EchoContext(ctx), tid, model.TypeTopic) + data["view_source"] = logic.DefaultViewSource.FindOne(context.EchoContext(ctx), tid, model.TypeTopic) + } + } else { + logic.Views.Incr(Request(ctx), model.TypeTopic, tid) + } + + return render(ctx, "topics/detail.html,common/comment.html", data) +} + +// Create 新建主题 +func (TopicController) Create(ctx echo.Context) error { + nid := goutils.MustInt(ctx.FormValue("nid")) + + me := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + // 请求新建主题页面 + if title == "" || ctx.Request().Method != "POST" { + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "activeTopics": "active", + "nid": nid, + "tab_list": hotNodes, + } + + if logic.NeedCaptcha(me) { + data["captchaId"] = captcha.NewLen(util.CaptchaLen) + } + + hadRecommend := false + if len(logic.AllRecommendNodes) > 0 { + hadRecommend = true + + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + data["had_recommend"] = hadRecommend + + return render(ctx, "topics/new.html", data) + } + + if nid == 0 { + return fail(ctx, 1, "没有选择节点!") + } + + forms, _ := ctx.FormParams() + tid, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 3, "内部服务错误:"+err.Error()) + } + + return success(ctx, map[string]interface{}{"tid": tid}) +} + +// Modify 修改主题 +func (TopicController) Modify(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + if ctx.Request().Method != "POST" { + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + hotNodes := logic.DefaultTopic.FindHotNodes(context.EchoContext(ctx)) + + data := map[string]interface{}{ + "topic": topics[0], + "activeTopics": "active", + "tab_list": hotNodes, + } + + hadRecommend := false + if len(logic.AllRecommendNodes) > 0 { + hadRecommend = true + + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + data["had_recommend"] = hadRecommend + + return render(ctx, "topics/new.html", data) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + _, err := logic.DefaultTopic.Publish(context.EchoContext(ctx), me, forms) + if err != nil { + if err == logic.NotModifyAuthorityErr { + return fail(ctx, 1, "没有权限操作") + } + + return fail(ctx, 2, "服务错误,请稍后重试!") + } + return success(ctx, map[string]interface{}{"tid": tid}) +} + +func (TopicController) Append(ctx echo.Context) error { + tid := goutils.MustInt(ctx.Param("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topics := logic.DefaultTopic.FindByTids([]int{tid}) + if len(topics) == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + topic := topics[0] + me := ctx.Get("user").(*model.Me) + if topic.Uid != me.Uid { + return ctx.Redirect(http.StatusSeeOther, "/topics/"+strconv.Itoa(tid)) + } + + // 请求新建主题页面 + if ctx.Request().Method != http.MethodPost { + data := map[string]interface{}{ + "topic": topic, + "activeTopics": "active", + } + + return render(ctx, "topics/append.html", data) + } + + content := ctx.FormValue("content") + err := logic.DefaultTopic.Append(context.EchoContext(ctx), me.Uid, tid, content) + if err != nil { + return fail(ctx, 1, "出错了:"+err.Error()) + } + + return success(ctx, nil) +} + +// Nodes 所有节点 +func (TopicController) Nodes(ctx echo.Context) error { + data := make(map[string]interface{}) + + if len(logic.AllRecommendNodes) > 0 { + data["nodes"] = logic.DefaultNode.FindAll(context.EchoContext(ctx)) + } else { + data["nodes"] = logic.GenNodes() + } + + return render(ctx, "topics/nodes.html", data) +} + +func (TopicController) SetTop(ctx echo.Context) error { + tid := goutils.MustInt(ctx.FormValue("tid")) + if tid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + me := ctx.Get("user").(*model.Me) + err := logic.DefaultTopic.SetTop(context.EchoContext(ctx), me, tid) + if err != nil { + if err == logic.NotFoundErr { + return ctx.Redirect(http.StatusSeeOther, "/topics") + } + + return fail(ctx, 1, "出错了:"+err.Error()) + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/user.go b/internal/http/controller/user.go new file mode 100644 index 00000000..595bc8ce --- /dev/null +++ b/internal/http/controller/user.go @@ -0,0 +1,253 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "html/template" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/slices" +) + +type UserController struct{} + +// 注册路由 +func (self UserController) RegisterRoute(g *echo.Group) { + g.GET("/user/:username", self.Home) + g.GET("/user/:username/topics", self.Topics) + g.GET("/user/:username/articles", self.Articles) + g.GET("/user/:username/resources", self.Resources) + g.GET("/user/:username/projects", self.Projects) + g.GET("/user/:username/comments", self.Comments) + g.GET("/users", self.ReadList) + g.Match([]string{"GET", "POST"}, "/user/email/unsubscribe", self.EmailUnsub) +} + +// Home 用户个人首页 +func (UserController) Home(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 || user.Status == model.UserStatusOutage { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + user.Weight = logic.DefaultRank.UserDAURank(context.EchoContext(ctx), user.Uid) + + topics := logic.DefaultTopic.FindRecent(5, user.Uid) + + articles := logic.DefaultArticle.FindByUser(context.EchoContext(ctx), user.Username, 5) + + resources := logic.DefaultResource.FindRecent(context.EchoContext(ctx), user.Uid) + for _, resource := range resources { + resource.CatName = logic.GetCategoryName(resource.Catid) + } + + projects := logic.DefaultProject.FindRecent(context.EchoContext(ctx), user.Username) + comments := logic.DefaultComment.FindRecent(context.EchoContext(ctx), user.Uid, -1, 5) + + user.IsOnline = logic.Book.RegUserIsOnline(user.Uid) + + return render(ctx, "user/profile.html", map[string]interface{}{ + "activeUsers": "active", + "topics": topics, + "articles": articles, + "resources": resources, + "projects": projects, + "comments": comments, + "user": user, + }) +} + +// ReadList 会员列表 +func (UserController) ReadList(ctx echo.Context) error { + // 获取活跃会员 + // activeUsers := logic.DefaultUser.FindActiveUsers(ctx, 36) + activeUsers := logic.DefaultRank.FindDAURank(context.EchoContext(ctx), 36) + // 获取最新加入会员 + newUsers := logic.DefaultUser.FindNewUsers(context.EchoContext(ctx), 36) + // 获取会员总数 + total := logic.DefaultUser.Total() + + return render(ctx, "user/users.html", map[string]interface{}{"activeUsers": "active", "actives": activeUsers, "news": newUsers, "total": total}) +} + +// EmailUnsub 邮件订阅/退订页面 +func (UserController) EmailUnsub(ctx echo.Context) error { + token := ctx.FormValue("u") + if token == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + // 校验 token 的合法性 + email := ctx.FormValue("email") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "email", email) + if user.Email == "" { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + realToken := logic.DefaultEmail.GenUnsubscribeToken(user) + if token != realToken { + return ctx.Redirect(http.StatusSeeOther, "/") + } + + if ctx.Request().Method != "POST" { + data := map[string]interface{}{ + "email": email, + "token": token, + "unsubscribe": user.Unsubscribe, + } + + return render(ctx, "user/email_unsub.html", data) + } + + logic.DefaultUser.EmailSubscribe(context.EchoContext(ctx), user.Uid, goutils.MustInt(ctx.FormValue("unsubscribe"))) + + return success(ctx, nil) +} + +func (UserController) Topics(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "uid=?" + topics := logic.DefaultTopic.FindAll(context.EchoContext(ctx), paginator, "topics.tid DESC", querystring, user.Uid) + total := logic.DefaultTopic.Count(context.EchoContext(ctx), querystring, user.Uid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/topics.html", map[string]interface{}{ + "user": user, + "activeTopics": "active", + "topics": topics, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Articles(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "author_txt=?" + articles := logic.DefaultArticle.FindAll(context.EchoContext(ctx), paginator, "id DESC", querystring, user.Username) + total := logic.DefaultArticle.Count(context.EchoContext(ctx), querystring, user.Username) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/articles.html", map[string]interface{}{ + "user": user, + "activeArticles": "active", + "articles": articles, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Resources(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "uid=?" + resources, total := logic.DefaultResource.FindAll(context.EchoContext(ctx), paginator, "resource.id DESC", querystring, user.Uid) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/resources.html", map[string]interface{}{ + "user": user, + "activeResources": "active", + "resources": resources, + "page": template.HTML(pageHtml), + "total": total, + }) +} + +func (UserController) Projects(ctx echo.Context) error { + username := ctx.Param("username") + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + querystring := "username=?" + projects := logic.DefaultProject.FindAll(context.EchoContext(ctx), paginator, "id DESC", querystring, user.Username) + total := logic.DefaultProject.Count(context.EchoContext(ctx), querystring, user.Username) + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + return render(ctx, "user/projects.html", map[string]interface{}{ + "user": user, + "activeProjects": "active", + "projects": projects, + "page": template.HTML(pageHtml), + "total": total, + }) +} +func (UserController) Comments(ctx echo.Context) error { + + username := ctx.Param("username") + + userid := 0 + querystring := "" + + if username != "0" { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "username", username) + if user == nil || user.Uid == 0 { + return ctx.Redirect(http.StatusSeeOther, "/users") + } + querystring = "uid=?" + userid = user.Uid + username = user.Username + } else { + username = "" + } + + curPage := goutils.MustInt(ctx.QueryParam("p"), 1) + paginator := logic.NewPaginator(curPage) + + comments := logic.DefaultComment.FindAll(context.EchoContext(ctx), paginator, "cid DESC", querystring, userid) + + total := logic.DefaultComment.Count(context.EchoContext(ctx), querystring, userid) + + pageHtml := paginator.SetTotal(total).GetPageHtml(ctx.Request().URL.Path) + + data := map[string]interface{}{ + "comments": comments, + "page": template.HTML(pageHtml), + "total": total, + } + + if username == "" { + uids := slices.StructsIntSlice(comments, "Uid") + data["users"] = logic.DefaultUser.FindUserInfos(context.EchoContext(ctx), uids) + } + + return render(ctx, "user/comments.html", data) + +} diff --git a/internal/http/controller/websocket.go b/internal/http/controller/websocket.go new file mode 100644 index 00000000..cd3f59d5 --- /dev/null +++ b/internal/http/controller/websocket.go @@ -0,0 +1,86 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "sync/atomic" + "time" + + "github.com/studygolang/studygolang/internal/logic" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "golang.org/x/net/websocket" +) + +type WebsocketController struct { + ServerId uint32 +} + +func (this *WebsocketController) RegisterRoute(g *echo.Group) { + g.GET("/ws", echo.WrapHandler(websocket.Handler(this.Ws))) +} + +// websocket,统计在线用户数 +// uri: /ws +func (this *WebsocketController) Ws(wsConn *websocket.Conn) { + defer wsConn.Close() + + serverId := int(atomic.AddUint32(&this.ServerId, 1)) + + isUid := true + req := wsConn.Request() + user := goutils.MustInt(req.FormValue("uid")) + if user == 0 { + user = int(goutils.Ip2long(goutils.RemoteIp(req))) + isUid = false + } + userData := logic.Book.AddUser(user, serverId, isUid) + // 给自己发送消息,告诉当前在线用户数、历史最高在线人数 + onlineInfo := map[string]int{"online": logic.Book.Len(), "maxonline": logic.MaxOnlineNum()} + message := logic.NewMessage(logic.WsMsgOnline, onlineInfo) + err := websocket.JSON.Send(wsConn, message) + if err != nil { + logger.Errorln("Sending onlineusers error:", err) + return + } + + messageChan := userData.MessageQueue(serverId) + + ticker := time.NewTicker(15e9) + defer ticker.Stop() + + var clientClosed = false + for { + select { + case message := <-messageChan: + if err := websocket.JSON.Send(wsConn, message); err != nil { + // logger.Errorln("Send message", message, " to user:", user, "server_id:", serverId, "error:", err) + clientClosed = true + } + // 心跳 + case <-ticker.C: + if err := websocket.JSON.Send(wsConn, ""); err != nil { + // logger.Errorln("Send heart message to user:", user, "server_id:", serverId, "error:", err) + clientClosed = true + } + } + if clientClosed { + logic.Book.DelUser(user, serverId, isUid) + logger.Infoln("user:", user, "client close") + break + } + } + // 用户退出时需要变更其他用户看到的在线用户数 + if !logic.Book.UserIsOnline(user) { + logger.Infoln("user:", user, "had leave") + + message := logic.NewMessage(logic.WsMsgOnline, map[string]int{"online": logic.Book.Len()}) + go logic.Book.BroadcastAllUsersMessage(message) + } +} diff --git a/internal/http/controller/wechat.go b/internal/http/controller/wechat.go new file mode 100644 index 00000000..4e80d6fa --- /dev/null +++ b/internal/http/controller/wechat.go @@ -0,0 +1,68 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "io/ioutil" + "net/http" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +type WechatController struct{} + +// 注册路由 +func (self WechatController) RegisterRoute(g *echo.Group) { + g.Any("/wechat/autoreply", self.AutoReply) + g.POST("/wechat/bind", self.Bind) +} + +func (self WechatController) AutoReply(ctx echo.Context) error { + // 配置微信(不校验,直接返回成功) + if ctx.QueryParam("echostr") != "" { + return ctx.String(http.StatusOK, ctx.QueryParam("echostr")) + } + + body, err := ioutil.ReadAll(ctx.Request().Body) + if err != nil { + return ctx.String(http.StatusOK, "") + } + + if len(body) == 0 { + return ctx.String(http.StatusOK, "") + } + + wechatReply, err := logic.DefaultWechat.AutoReply(context.EchoContext(ctx), body) + if err != nil { + return ctx.String(http.StatusOK, "") + } + + return ctx.XML(http.StatusOK, wechatReply) +} + +func (self WechatController) Bind(ctx echo.Context) error { + captcha := ctx.FormValue("captcha") + if captcha == "" { + return fail(ctx, 1, "验证码是不能空") + } + + echoCtx := context.EchoContext(ctx) + me, ok := ctx.Get("user").(*model.Me) + if !ok { + return fail(ctx, 1, "必须先登录") + } + err := logic.DefaultWechat.CheckCaptchaAndBind(echoCtx, me, captcha) + if err != nil { + return fail(ctx, 2, "验证码错误,请确认获取了或没填错!") + } + + return success(ctx, nil) +} diff --git a/internal/http/controller/wide.go b/internal/http/controller/wide.go new file mode 100644 index 00000000..a669b29e --- /dev/null +++ b/internal/http/controller/wide.go @@ -0,0 +1,21 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import echo "github.com/labstack/echo/v4" + +type WideController struct{} + +// 注册路由 +func (self WideController) RegisterRoute(g *echo.Group) { + g.GET("/wide/playground", self.Playground) +} + +// Playground Wide 的内嵌 iframe 的 playground +func (WideController) Playground(ctx echo.Context) error { + return render(ctx, "wide/playground.html", nil) +} diff --git a/internal/http/controller/wiki.go b/internal/http/controller/wiki.go new file mode 100644 index 00000000..35c9d1dc --- /dev/null +++ b/internal/http/controller/wiki.go @@ -0,0 +1,165 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package controller + +import ( + "net/http" + + "github.com/studygolang/studygolang/context" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/http/middleware" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" +) + +// 在需要评论(喜欢)且要回调的地方注册评论(喜欢)对象 +func init() { + // 注册评论(喜欢)对象 + // logic.RegisterCommentObject(model.TypeArticle, logic.ArticleComment{}) + // logic.RegisterLikeObject(model.TypeArticle, logic.ArticleLike{}) +} + +type WikiController struct{} + +// 注册路由 +func (self WikiController) RegisterRoute(g *echo.Group) { + g.Match([]string{"GET", "POST"}, "/wiki/new", self.Create, middleware.NeedLogin(), middleware.Sensivite(), middleware.BalanceCheck()) + g.Match([]string{"GET", "POST"}, "/wiki/modify", self.Modify, middleware.NeedLogin(), middleware.Sensivite()) + g.GET("/wiki", self.ReadList) + g.GET("/wiki/:uri", self.Detail) +} + +// Create 创建wiki页 +func (WikiController) Create(ctx echo.Context) error { + title := ctx.FormValue("title") + // 请求新建 wiki 页面 + if title == "" || ctx.Request().Method != "POST" { + return render(ctx, "wiki/new.html", map[string]interface{}{"activeWiki": "active"}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultWiki.Create(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误") + } + + return success(ctx, nil) +} + +// Modify 修改 Wiki 页 +func (WikiController) Modify(ctx echo.Context) error { + id := goutils.MustInt(ctx.FormValue("id")) + if id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + if ctx.Request().Method != "POST" { + wiki := logic.DefaultWiki.FindById(context.EchoContext(ctx), id) + if wiki.Id == 0 { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + return render(ctx, "wiki/new.html", map[string]interface{}{"activeWiki": "active", "wiki": wiki}) + } + + me := ctx.Get("user").(*model.Me) + forms, _ := ctx.FormParams() + err := logic.DefaultWiki.Modify(context.EchoContext(ctx), me, forms) + if err != nil { + return fail(ctx, 1, "内部服务错误") + } + + return success(ctx, nil) +} + +// Detail 展示wiki页 +func (WikiController) Detail(ctx echo.Context) error { + wiki := logic.DefaultWiki.FindOne(context.EchoContext(ctx), ctx.Param("uri")) + if wiki == nil { + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + // likeFlag := 0 + me, ok := ctx.Get("user").(*model.Me) + if ok { + // likeFlag = logic.DefaultLike.HadLike(ctx, me.Uid, wiki.Id, model.TypeWiki) + logic.Views.Incr(Request(ctx), model.TypeWiki, wiki.Id, me.Uid) + } else { + logic.Views.Incr(Request(ctx), model.TypeWiki, wiki.Id) + } + + // 为了阅读数即时看到 + wiki.Viewnum++ + + return render(ctx, "wiki/content.html", map[string]interface{}{"activeWiki": "active", "wiki": wiki}) +} + +// ReadList 获得wiki列表 +func (WikiController) ReadList(ctx echo.Context) error { + limit := 20 + + lastId := goutils.MustInt(ctx.QueryParam("lastid")) + wikis := logic.DefaultWiki.FindBy(context.EchoContext(ctx), limit+5, lastId) + if wikis == nil { + logger.Errorln("wiki controller: find wikis error") + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + num := len(wikis) + if num == 0 { + if lastId == 0 { + return ctx.Redirect(http.StatusSeeOther, "/") + } + return ctx.Redirect(http.StatusSeeOther, "/wiki") + } + + var ( + hasPrev, hasNext bool + prevId, nextId int + ) + + if lastId != 0 { + prevId = lastId + + // 避免因为wiki下线,导致判断错误(所以 > 5) + if prevId-wikis[0].Id > 5 { + hasPrev = false + } else { + prevId += limit + hasPrev = true + } + } + + if num > limit { + hasNext = true + wikis = wikis[:limit] + nextId = wikis[limit-1].Id + } else { + nextId = wikis[num-1].Id + } + + pageInfo := map[string]interface{}{ + "has_prev": hasPrev, + "prev_id": prevId, + "has_next": hasNext, + "next_id": nextId, + } + + // 获取当前用户喜欢对象信息 + // me, ok := ctx.Get("user").(*model.Me) + // var likeFlags map[int]int + // if ok { + // likeFlags, _ = logic.DefaultLike.FindUserLikeObjects(ctx, me.Uid, model.TypeWiki, wikis[0].Id, nextId) + // } + + return render(ctx, "wiki/list.html", map[string]interface{}{"wikis": wikis, "activeWiki": "active", "page": pageInfo}) +} diff --git a/internal/http/http.go b/internal/http/http.go new file mode 100644 index 00000000..0b17c568 --- /dev/null +++ b/internal/http/http.go @@ -0,0 +1,479 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package http + +import ( + "bytes" + "encoding/json" + "html/template" + "math" + "math/rand" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/global" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/gorilla/sessions" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/config" + "github.com/polaris1119/goutils" + "github.com/polaris1119/logger" + "github.com/polaris1119/times" +) + +var Store = sessions.NewCookieStore([]byte(config.ConfigFile.MustValue("global", "cookie_secret"))) + +func SetLoginCookie(ctx echo.Context, username string) { + Store.Options.HttpOnly = true + + session := GetCookieSession(ctx) + if ctx.FormValue("remember_me") != "1" { + // 浏览器关闭,cookie删除,否则保存30天(github.com/gorilla/sessions 包的默认值) + session.Options = &sessions.Options{ + Path: "/", + HttpOnly: true, + } + } + session.Values["username"] = username + req := Request(ctx) + resp := ResponseWriter(ctx) + session.Save(req, resp) +} + +func SetCookie(ctx echo.Context, key, value string) { + Store.Options.HttpOnly = true + + session := GetCookieSession(ctx) + session.Values[key] = value + req := Request(ctx) + resp := ResponseWriter(ctx) + session.Save(req, resp) +} + +func GetFromCookie(ctx echo.Context, key string) string { + session := GetCookieSession(ctx) + val, ok := session.Values[key] + if ok { + return val.(string) + } + return "" +} + +// 必须是 http.Request +func GetCookieSession(ctx echo.Context) *sessions.Session { + session, _ := Store.Get(Request(ctx), "user") + return session +} + +func Request(ctx echo.Context) *http.Request { + return ctx.Request() +} + +func ResponseWriter(ctx echo.Context) http.ResponseWriter { + return ctx.Response() +} + +// 自定义模板函数 +var funcMap = template.FuncMap{ + // 获取gravatar头像 + "gravatar": util.Gravatar, + // 转为前端显示需要的时间格式 + "formatTime": func(i interface{}) string { + ctime, ok := i.(string) + if !ok { + return "" + } + t, _ := time.Parse("2006-01-02 15:04:05", ctime) + return t.Format(time.RFC3339) + "+08:00" + }, + "format": func(i interface{}, format string) string { + switch i.(type) { + case time.Time: + return (i.(time.Time)).Format(format) + case int64: + val := i.(int64) + return time.Unix(val, 0).Format(format) + } + + return "" + }, + "hasPrefix": func(s, prefix string) bool { + if strings.HasPrefix(s, prefix) { + return true + } + return false + }, + "substring": util.Substring, + "add": func(nums ...interface{}) int { + total := 0 + for _, num := range nums { + if n, ok := num.(int); ok { + total += n + } + } + return total + }, + "sub": func(num1, num2 int) int { + return num1 - num2 + }, + "mod": func(num1, num2 int) int { + if num1 == 0 { + num1 = rand.Intn(500) + } + + return num1 % num2 + }, + "divide": func(num1, num2 int) int { + return int(math.Ceil(float64(num1) / float64(num2))) + }, + "explode": func(s, sep string) []string { + return strings.Split(s, sep) + }, + "noescape": func(s string) template.HTML { + return template.HTML(s) + }, + "timestamp": func(ts ...time.Time) int64 { + if len(ts) > 0 { + return ts[0].Unix() + } + return time.Now().Unix() + }, + "distanceDay": func(i interface{}) int { + var ( + t time.Time + err error + ) + switch val := i.(type) { + case string: + t, err = time.ParseInLocation("2006-01-02 15:04:05", val, time.Local) + if err != nil { + return 0 + } + case time.Time: + t = val + case model.OftenTime: + t = time.Time(val) + } + + return int(time.Now().Sub(t).Hours() / 24) + }, + "canEdit": logic.CanEdit, + "canPublish": logic.CanPublish, + "parseJSON": func(str string) map[string]interface{} { + result := make(map[string]interface{}) + json.Unmarshal([]byte(str), &result) + return result + }, + "safeHtml": util.SafeHtml, + "imageUrl": func(uri string, isHttps bool) string { + if !strings.HasPrefix(uri, "http") { + cdnDomain := global.App.CanonicalCDN(isHttps) + return cdnDomain + uri + } + + return uri + }, + "genList": func(n int, steps ...int) []int { + step := 1 + if len(steps) > 0 { + step = steps[0] + } + num := int(math.Ceil(float64(n) / float64(step))) + nums := make([]int, num) + for i := 0; i < num; i++ { + nums[i] = i + 1 + } + + return nums + }, +} + +func tplInclude(file string, dot map[string]interface{}) template.HTML { + var buffer = &bytes.Buffer{} + tpl, err := template.New(filepath.Base(file)).Funcs(funcMap).ParseFiles(config.TemplateDir + file) + // tpl, err := template.ParseFiles(config.TemplateDir + file) + if err != nil { + logger.Errorf("parse template file(%s) error:%v\n", file, err) + return "" + } + err = tpl.Execute(buffer, dot) + if err != nil { + logger.Errorf("template file(%s) syntax error:%v", file, err) + return "" + } + return template.HTML(buffer.String()) +} + +const ( + LayoutTpl = "common/layout.html" + AdminLayoutTpl = "common.html" +) + +// 是否访问这些页面 +var filterPathes = map[string]struct{}{ + "/account/login": {}, + "/account/register": {}, + "/account/forgetpwd": {}, + "/account/resetpwd": {}, + "/topics/new": {}, + "/topics/modify": {}, + "/resources/new": {}, + "/resources/modify": {}, + "/articles/new": {}, + "/articles/modify": {}, + "/project/new": {}, + "/project/modify": {}, + "/book/new": {}, + "/wiki/new": {}, + "/wiki/modify": {}, +} + +// Render html 输出 +func Render(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + if data == nil { + data = map[string]interface{}{} + } + + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = LayoutTpl + "," + contentTpl + // 为了使用自定义的模板函数,首先New一个以第一个模板文件名为模板名。 + // 这样,在ParseFiles时,新返回的*Template便还是原来的模板实例 + htmlFiles := strings.Split(contentTpl, ",") + for i, contentTpl := range htmlFiles { + htmlFiles[i] = config.TemplateDir + contentTpl + } + tpl, err := template.New("layout.html").Funcs(funcMap). + Funcs(template.FuncMap{"include": tplInclude}).ParseFiles(htmlFiles...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", Request(ctx).RequestURI, err) + return err + } + + if strings.Contains(ctx.Request().UserAgent(), "miniProgram") { + data["min_program"] = true + } else { + data["pos_ad"] = logic.DefaultAd.FindAll(context.EchoContext(ctx), ctx.Path()) + } + + data["cur_time"] = times.Format("Y-m-d H:i:s") + data["path"] = ctx.Path() + data["filter"] = false + if _, ok := filterPathes[ctx.Path()]; ok { + data["filter"] = true + } + + // TODO:每次查询有点影响性能 + hasLoginMisson := false + me, ok := ctx.Get("user").(*model.Me) + if ok { + // 每日登录奖励 + hasLoginMisson = logic.DefaultMission.HasLoginMission(context.EchoContext(ctx), me) + } + data["has_login_misson"] = hasLoginMisson + + return executeTpl(ctx, tpl, data) +} + +// RenderAdmin html 输出 +func RenderAdmin(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + if data == nil { + data = map[string]interface{}{} + } + + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = AdminLayoutTpl + "," + contentTpl + // 为了使用自定义的模板函数,首先New一个以第一个模板文件名为模板名。 + // 这样,在ParseFiles时,新返回的*Template便还是原来的模板实例 + htmlFiles := strings.Split(contentTpl, ",") + for i, contentTpl := range htmlFiles { + htmlFiles[i] = config.TemplateDir + "admin/" + contentTpl + } + + requestURI := Request(ctx).RequestURI + tpl, err := template.New("common.html").Funcs(funcMap).ParseFiles(htmlFiles...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + // 当前用户信息 + curUser := ctx.Get("user").(*model.Me) + + if menu1, menu2, curMenu1 := logic.DefaultAuthority.GetUserMenu(context.EchoContext(ctx), curUser, requestURI); menu2 != nil { + data["menu1"] = menu1 + data["menu2"] = menu2 + data["uri"] = requestURI + data["cur_menu1"] = curMenu1 + } + + return executeTpl(ctx, tpl, data) +} + +// 后台 query 查询返回结果 +func RenderQuery(ctx echo.Context, contentTpl string, data map[string]interface{}) error { + objLog := logic.GetLogger(context.EchoContext(ctx)) + + contentTpl = "common_query.html," + contentTpl + contentTpls := strings.Split(contentTpl, ",") + for i, contentTpl := range contentTpls { + contentTpls[i] = config.TemplateDir + "admin/" + strings.TrimSpace(contentTpl) + } + + requestURI := Request(ctx).RequestURI + tpl, err := template.New("common_query.html").Funcs(funcMap).ParseFiles(contentTpls...) + if err != nil { + objLog.Errorf("解析模板出错(ParseFiles):[%q] %s\n", requestURI, err) + return err + } + + buf := new(bytes.Buffer) + err = tpl.Execute(buf, data) + if err != nil { + objLog.Errorf("执行模板出错(Execute):[%q] %s\n", requestURI, err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func executeTpl(ctx echo.Context, tpl *template.Template, data map[string]interface{}) error { + objLog := logic.GetLogger(context.EchoContext(ctx)) + + // 如果没有定义css和js模板,则定义之 + if jsTpl := tpl.Lookup("js"); jsTpl == nil { + tpl.Parse(`{{define "js"}}{{end}}`) + } + if cssTpl := tpl.Lookup("css"); cssTpl == nil { + tpl.Parse(`{{define "css"}}{{end}}`) + } + // 如果没有 seo 模板,则定义之 + if seoTpl := tpl.Lookup("seo"); seoTpl == nil { + tpl.Parse(`{{define "seo"}} + + + {{end}}`) + } + + // 当前用户信息 + curUser, ok := ctx.Get("user").(*model.Me) + if ok { + data["me"] = curUser + } else { + data["me"] = &model.Me{} + } + + // websocket主机 + if global.OnlineEnv() { + data["wshost"] = global.App.Domain + } else { + data["wshost"] = global.App.Host + ":" + global.App.Port + } + global.App.SetUptime() + global.App.SetCopyright() + + isHttps := CheckIsHttps(ctx) + cdnDomain := global.App.CanonicalCDN(isHttps) + if isHttps { + global.App.BaseURL = "https://" + global.App.Domain + "/" + } else { + global.App.BaseURL = "http://" + global.App.Domain + "/" + } + + staticDomain := "" + if global.OnlineEnv() { + staticDomain = strings.TrimRight(cdnDomain, "/") + } + + data["app"] = global.App + data["is_https"] = isHttps + data["cdn_domain"] = cdnDomain + data["static_domain"] = staticDomain + data["is_pro"] = global.OnlineEnv() + + data["online_users"] = map[string]int{"online": logic.Book.Len(), "maxonline": logic.MaxOnlineNum()} + + data["setting"] = logic.WebsiteSetting + + // 评论每页显示多少个 + data["cmt_per_num"] = logic.CommentPerNum + + // 记录处理时间 + data["resp_time"] = time.Since(ctx.Get("req_start_time").(time.Time)) + + buf := new(bytes.Buffer) + err := tpl.Execute(buf, data) + if err != nil { + objLog.Errorln("excute template error:", err) + return err + } + + return ctx.HTML(http.StatusOK, buf.String()) +} + +func CheckIsHttps(ctx echo.Context) bool { + isHttps := goutils.MustBool(ctx.Request().Header.Get("X-Https")) + if logic.WebsiteSetting.OnlyHttps { + isHttps = true + } + + return isHttps +} + +///////////////////////////////// APP 相关 ////////////////////////////// + +const ( + TokenSalt = "b3%JFOykZx_golang_polaris" + NeedReLoginCode = 600 +) + +func ParseToken(token string) (int, bool) { + if len(token) < 32 { + return 0, false + } + + pos := strings.LastIndex(token, "uid") + if pos == -1 { + return 0, false + } + return goutils.MustInt(token[pos+3:]), true +} + +func ValidateToken(token string) bool { + _, ok := ParseToken(token) + if !ok { + return false + } + + expireTime := time.Unix(goutils.MustInt64(token[:10]), 0) + if time.Now().Before(expireTime) { + return true + } + return false +} + +func GenToken(uid int) string { + expireTime := time.Now().Add(30 * 24 * time.Hour).Unix() + + buffer := goutils.NewBuffer().Append(expireTime).Append(uid).Append(TokenSalt) + + md5 := goutils.Md5(buffer.String()) + + buffer = goutils.NewBuffer().Append(expireTime).Append(md5).Append("uid").Append(uid) + return buffer.String() +} + +func AccessControl(ctx echo.Context) { + ctx.Response().Header().Add("Access-Control-Allow-Origin", "*") +} diff --git a/internal/http/internal/helper/account.go b/internal/http/internal/helper/account.go new file mode 100644 index 00000000..3a5d9ac7 --- /dev/null +++ b/internal/http/internal/helper/account.go @@ -0,0 +1,56 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of self source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package helper + +import ( + "sync" + + "github.com/polaris1119/logger" + guuid "github.com/twinj/uuid" +) + +// 保存uuid和email的对应关系(TODO:重启如何处理,有效期问题) +type regActivateCode struct { + data map[string]string + locker sync.RWMutex +} + +var RegActivateCode = ®ActivateCode{ + data: map[string]string{}, +} + +func (this *regActivateCode) GenUUID(email string) string { + this.locker.Lock() + defer this.locker.Unlock() + var uuid string + for { + uuid = guuid.NewV4().String() + if _, ok := this.data[uuid]; !ok { + this.data[uuid] = email + break + } + logger.Errorln("GenUUID 冲突....") + } + return uuid +} + +func (this *regActivateCode) GetEmail(uuid string) (email string, ok bool) { + this.locker.RLock() + defer this.locker.RUnlock() + + if email, ok = this.data[uuid]; ok { + return + } + return +} + +func (this *regActivateCode) DelUUID(uuid string) { + this.locker.Lock() + defer this.locker.Unlock() + + delete(this.data, uuid) +} diff --git a/internal/http/middleware/admin.go b/internal/http/middleware/admin.go new file mode 100644 index 00000000..5686c1da --- /dev/null +++ b/internal/http/middleware/admin.go @@ -0,0 +1,33 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// AdminAuth 用于 echo 框架的判断用户是否有管理后台权限 +func AdminAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user := ctx.Get("user").(*model.Me) + if !user.IsAdmin { + return ctx.HTML(http.StatusForbidden, `403 Forbidden`) + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/balance_check.go b/internal/http/middleware/balance_check.go new file mode 100644 index 00000000..0d449ff2 --- /dev/null +++ b/internal/http/middleware/balance_check.go @@ -0,0 +1,49 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" +) + +// BalanceCheck 用于 echo 框架,用户发布内容校验余额是否足够 +func BalanceCheck() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + + if util.IsAjax(ctx) { + + curUser := ctx.Get("user").(*model.Me) + + title := ctx.FormValue("title") + content := ctx.FormValue("content") + if ctx.Request().Method == "POST" && (title != "" || content != "") { + if ctx.Path() == "/comment/:objid" { + if curUser.Balance < 5 { + return ctx.String(http.StatusOK, `{"ok":0,"error":"对不起,您的账号余额不足,可以领取初始资本!"}`) + } + } else { + if curUser.Balance < 20 { + return ctx.String(http.StatusOK, `{"ok":0,"error":"对不起,您的账号余额不足,可以领取初始资本!"}`) + } + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/captcha.go b/internal/http/middleware/captcha.go new file mode 100644 index 00000000..a659ea89 --- /dev/null +++ b/internal/http/middleware/captcha.go @@ -0,0 +1,44 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/dchest/captcha" + echo "github.com/labstack/echo/v4" +) + +// CheckCaptcha 用于 echo 框架校验发布验证码 +func CheckCaptcha() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + + curUser := ctx.Get("user").(*model.Me) + + if ctx.Request().Method == "POST" { + if logic.NeedCaptcha(curUser) { + captchaId := ctx.FormValue("captchaid") + if !captcha.VerifyString(captchaId, ctx.FormValue("captchaSolution")) { + util.SetCaptcha(captchaId) + return ctx.String(http.StatusOK, `{"ok":0,"error":"验证码错误,记得刷新验证码!"}`) + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/http_error.go b/internal/http/middleware/http_error.go new file mode 100644 index 00000000..99dda9ab --- /dev/null +++ b/internal/http/middleware/http_error.go @@ -0,0 +1,51 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/util" + + echo "github.com/labstack/echo/v4" +) + +// HTTPError 用于 echo 框架的 HTTP 错误 +func HTTPError() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if err := next(ctx); err != nil { + + if !ctx.Response().Committed { + if he, ok := err.(*echo.HTTPError); ok { + switch he.Code { + case http.StatusNotFound: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"接口不存在"}`) + } + return Render(ctx, "404.html", nil) + case http.StatusForbidden: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"没有权限访问"}`) + } + return Render(ctx, "403.html", map[string]interface{}{"msg": he.Message}) + case http.StatusInternalServerError: + if util.IsAjax(ctx) { + return ctx.String(http.StatusOK, `{"ok":0,"error":"接口服务器错误"}`) + } + return Render(ctx, "500.html", nil) + default: + return err + } + } + } + } + return nil + } + } +} diff --git a/internal/http/middleware/installed.go b/internal/http/middleware/installed.go new file mode 100644 index 00000000..6a48273a --- /dev/null +++ b/internal/http/middleware/installed.go @@ -0,0 +1,46 @@ +// Copyright 2016 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + "strings" + + "github.com/studygolang/studygolang/db" + + echo "github.com/labstack/echo/v4" +) + +// Installed 用于 echo 框架,判断是否已经安装了 +func Installed(filterPrefixs []string) echo.MiddlewareFunc { + filterPrefixs = append(filterPrefixs, "/install") + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if db.MasterDB == nil { + shouldRedirect := true + + uri := ctx.Request().RequestURI + for _, prefix := range filterPrefixs { + if strings.HasPrefix(uri, prefix) { + shouldRedirect = false + break + } + } + + if shouldRedirect { + return ctx.Redirect(http.StatusSeeOther, "/install") + } + } + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} diff --git a/internal/http/middleware/login.go b/internal/http/middleware/login.go new file mode 100644 index 00000000..c4b043c8 --- /dev/null +++ b/internal/http/middleware/login.go @@ -0,0 +1,158 @@ +// Copyright 2013 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "time" + + mycontext "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/db" + . "github.com/studygolang/studygolang/internal/http" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + "github.com/studygolang/studygolang/util" + + "github.com/gorilla/context" + echo "github.com/labstack/echo/v4" + "github.com/polaris1119/goutils" +) + +// AutoLogin 用于 echo 框架的自动登录和通过 cookie 获取用户信息 +func AutoLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + // github.com/gorilla/sessions 要求必须 Clear + defer context.Clear(Request(ctx)) + + ctx.Set("req_start_time", time.Now()) + + var getCurrentUser = func(usernameOrId interface{}) { + ip := goutils.RemoteIp(Request(ctx)) + // IP 黑名单,不让登录 + if logic.DefaultRisk.IsBlackIP(ip) { + return + } + + if db.MasterDB != nil { + ctx.Set("ip", ip) + // TODO: 考虑缓存,或延迟查询,避免每次都查询 + user := logic.DefaultUser.FindCurrentUser(mycontext.EchoContext(ctx), usernameOrId) + if user.Uid != 0 { + ctx.Set("user", user) + + if !util.IsAjax(ctx) && ctx.Path() != "/ws" { + go logic.ViewObservable.NotifyObservers(user.Uid, 0, 0) + } + } + } + } + + session := GetCookieSession(ctx) + username, ok := session.Values["username"] + if ok { + getCurrentUser(username) + } else { + // App(手机) 登录 + uid, ok := ParseToken(ctx.FormValue("token")) + if ok { + getCurrentUser(uid) + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +// NeedLogin 用于 echo 框架的验证必须登录的请求 +func NeedLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user, ok := ctx.Get("user").(*model.Me) + if !ok || user.Status != model.UserStatusAudit { + method := ctx.Request().Method + if util.IsAjax(ctx) { + if !strings.HasPrefix(ctx.Path(), "/account") { + return ctx.JSON(http.StatusForbidden, map[string]interface{}{"ok": 0, "error": "403 Forbidden"}) + } + } else { + if method == "POST" { + return ctx.HTML(http.StatusForbidden, `403 Forbidden`) + } + + if !ok { + reqURL := ctx.Request().URL + uri := reqURL.Path + if reqURL.RawQuery != "" { + uri += "?" + reqURL.RawQuery + } + return ctx.Redirect(http.StatusSeeOther, "/account/login?redirect_uri="+url.QueryEscape(uri)) + } else { + // 未激活可以查看账号信息 + if !strings.HasPrefix(ctx.Path(), "/account") { + return echo.NewHTTPError(http.StatusForbidden, `您的邮箱未激活,去激活`) + } + } + } + } else { + newUserWait := time.Duration(logic.UserSetting[model.KeyNewUserWait]) * time.Second + if newUserWait > 0 { + elapse := time.Now().Sub(user.CreatedAt) + if elapse <= newUserWait { + return echo.NewHTTPError(http.StatusForbidden, `您需要再等待`+(newUserWait-elapse).String()+"才能进行此操作") + } + } + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +// AppNeedLogin 用于 echo 框架的验证必须登录的请求(APP 专用) +func AppNeedLogin() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + user, ok := ctx.Get("user").(*model.Me) + if ok { + // 校验 token 是否有效 + if !ValidateToken(ctx.QueryParam("token")) { + return outputAppJSON(ctx, NeedReLoginCode, "token无效,请重新登录!") + } + + if user.Status != model.UserStatusAudit { + return outputAppJSON(ctx, 1, "账号未审核通过、被冻结或被停号,请联系我们") + } + } else { + return outputAppJSON(ctx, NeedReLoginCode, "请先登录!") + } + + if err := next(ctx); err != nil { + return err + } + + return nil + } + } +} + +func outputAppJSON(ctx echo.Context, code int, msg string) error { + AccessControl(ctx) + return ctx.JSON(http.StatusForbidden, map[string]interface{}{"code": strconv.Itoa(code), "msg": msg}) +} diff --git a/internal/http/middleware/notice.go b/internal/http/middleware/notice.go new file mode 100644 index 00000000..fabe6e07 --- /dev/null +++ b/internal/http/middleware/notice.go @@ -0,0 +1,50 @@ +// Copyright 2017 The StudyGolang Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// http://studygolang.com +// Author: polaris polaris@studygolang.com + +package middleware + +import ( + "fmt" + + "github.com/studygolang/studygolang/context" + "github.com/studygolang/studygolang/internal/logic" + "github.com/studygolang/studygolang/internal/model" + + echo "github.com/labstack/echo/v4" +) + +// PublishNotice 用于 echo 框架,用户发布内容邮件通知站长 +func PublishNotice() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + if err := next(ctx); err != nil { + return err + } + + curUser := ctx.Get("user").(*model.Me) + if curUser.IsRoot { + return nil + } + + title := ctx.FormValue("title") + content := ctx.FormValue("content") + if ctx.Request().Method == "POST" && (title != "" || content != "") { + requestURI := ctx.Request().RequestURI + go func() { + user := logic.DefaultUser.FindOne(context.EchoContext(ctx), "is_root", 1) + if user.Uid == 0 { + return + } + + content = fmt.Sprintf("URI:%sjsdifflib<\/a> ";d+="and John Resig's diff<\/a> ";d+="by Richard Bondi<\/a>";h.push(t=e("th","author",d));t.setAttribute("colspan",a?3:4);o.push(t=document.createElement("tbody"));for(i in h)h.hasOwnProperty(i)&&t.appendChild(h[i]);t=ut("table","diff"+(a?" inlinediff":""));for(i in o)o.hasOwnProperty(i)&&t.appendChild(o[i]);return t}};
+//! moment.js
+//! version : 2.15.2
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(n,t){typeof exports=="object"&&typeof module!="undefined"?module.exports=t():typeof define=="function"&&define.amd?define(t):n.moment=t()})(this,function(){"use strict";function i(){return lo.apply(null,arguments)}function pl(n){lo=n}function ei(n){return n instanceof Array||Object.prototype.toString.call(n)==="[object Array]"}function ou(n){return n!=null&&Object.prototype.toString.call(n)==="[object Object]"}function wl(n){for(var t in n)return!1;return!0}function su(n){return n instanceof Date||Object.prototype.toString.call(n)==="[object Date]"}function ao(n,t){for(var r=[],i=0;i This is a p This is a p This is a p 可能因为:>1}c--;c==0&&(c=Math.pow(2,s),s++);delete v[o]}else for(f=l[o],e=0;e>1;c--;c==0&&(c=Math.pow(2,s),s++);l[y]=w++;o=String(a)}if(o!==""){if(Object.prototype.hasOwnProperty.call(v,o)){if(o.charCodeAt(0)<256){for(e=0;e>1}else{for(f=1,e=0;e>1}c--;c==0&&(c=Math.pow(2,s),s++);delete v[o]}else for(f=l[o],e=0;e>1;c--;c==0&&(c=Math.pow(2,s),s++)}for(f=2,e=0;e>1;for(;;)if(r=r<<1,u==t-1){h.push(i(r));break}else u++;return h.join("")},decompress:function(t){return t==null?"":t==""?null:n._decompress(t.length,32768,function(n){return t.charCodeAt(n)})},_decompress:function(n,i,r){for(var c=[],k,l=4,a=4,v=3,y="",b=[],w,e,o,s,f,h,u={val:r(0),position:i,index:1},p=0;p<3;p+=1)c[p]=p;for(e=0,s=Math.pow(2,2),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;switch(k=e){case 0:for(e=0,s=Math.pow(2,8),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;h=t(e);break;case 1:for(e=0,s=Math.pow(2,16),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;h=t(e);break;case 2:return""}for(c[3]=h,w=h,b.push(h);;){if(u.index>n)return"";for(e=0,s=Math.pow(2,v),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;switch(h=e){case 0:for(e=0,s=Math.pow(2,8),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;c[a++]=t(e);h=a-1;l--;break;case 1:for(e=0,s=Math.pow(2,16),f=1;f!=s;)o=u.val&u.position,u.position>>=1,u.position==0&&(u.position=i,u.val=r(u.index++)),e|=(o>0?1:0)*f,f<<=1;c[a++]=t(e);h=a-1;l--;break;case 2:return b.join("")}if(l==0&&(l=Math.pow(2,v),v++),c[h])y=c[h];else if(h===a)y=w+w.charAt(0);else return null;b.push(y);c[a++]=w+y.charAt(0);l--;w=y;l==0&&(l=Math.pow(2,v),v++)}}};return n}();typeof define=="function"&&define.amd?define(function(){return LZString}):typeof module!="undefined"&&module!=null&&(module.exports=LZString);
\ No newline at end of file
diff --git a/static/ckeditor/plugins/autosave/lang/en.js b/static/ckeditor/plugins/autosave/lang/en.js
new file mode 100755
index 00000000..9472a0c4
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/en.js
@@ -0,0 +1,18 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'en', {
+ dateFormat: 'LLL',
+ autoSaveMessage: 'Auto Saved',
+ loadSavedContent: 'An auto-saved version of this content from "{0}" has been found. Would you like to compare content versions and choose which one to load? Clicking Cancel will remove previously auto-saved content.',
+ title: 'Compare auto-saved content with that loaded from the website',
+ loadedContent: 'Loaded content',
+ localStorageFull: 'Browser localStorage is full, clear your storage or Increase database size',
+ autoSavedContent: 'Auto-saved content from: \'',
+ ok: 'Yes, load auto-saved content',
+ no: 'No',
+ diffType: 'Choose view type:',
+ sideBySide: 'Side by side view',
+ inline: 'Inline view'
+});
diff --git a/static/ckeditor/plugins/autosave/lang/zh-cn.js b/static/ckeditor/plugins/autosave/lang/zh-cn.js
new file mode 100755
index 00000000..a19eeb86
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/zh-cn.js
@@ -0,0 +1,18 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'zh-cn', {
+ dateFormat: 'LLL',
+ autoSaveMessage: '自动保存',
+ loadSavedContent: '已经存在这个内容之前保存的版本 "{0}". 你希望比较两个不同的版本并从中选择你想要加载的那个么?',
+ title: '比较自动保存的版本',
+ loadedContent: '已经加载的内容',
+ localStorageFull: 'Browser localStorage is full, clear your storage or Increase database size',
+ autoSavedContent: '自动保存的内容: \'',
+ ok: '我确认,加载自动保存的内容',
+ no: '取消',
+ diffType: '选择视图类型:',
+ sideBySide: '并排视图',
+ inline: '内嵌视图'
+});
diff --git a/static/ckeditor/plugins/autosave/lang/zh.js b/static/ckeditor/plugins/autosave/lang/zh.js
new file mode 100755
index 00000000..143450ce
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/lang/zh.js
@@ -0,0 +1,17 @@
+/*
+Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
+For licensing, see LICENSE.html or http://ckeditor.com/license
+*/
+CKEDITOR.plugins.setLang('autosave', 'zh', {
+ dateFormat: 'LLL',
+ autoSaveMessage: '自動儲存 :)',
+ loadSavedContent: '你在 “{0}” 有一個自動儲存的版本。你想要比較新舊內容,並選擇使用哪一個嗎?點取消按鈕將會移除先前的自動儲存紀錄。',
+ title: '新舊內容比較',
+ loadedContent: '目前內容',
+ autoSavedContent: '自動儲存的內容: \'',
+ ok: '是,我要讀取自動儲存的內容',
+ no: '否',
+ diffType: '選擇比較方式:',
+ sideBySide: '雙欄式',
+ inline: '條列式'
+});
diff --git a/static/ckeditor/plugins/autosave/plugin.js b/static/ckeditor/plugins/autosave/plugin.js
new file mode 100755
index 00000000..86f0c45c
--- /dev/null
+++ b/static/ckeditor/plugins/autosave/plugin.js
@@ -0,0 +1,334 @@
+/**
+ * @license Copyright (c) CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.html or http://ckeditor.com/license
+ */
+
+(function() {
+ if (!supportsLocalStorage()) {
+ CKEDITOR.plugins.add("autosave", {}); //register a dummy plugin to pass CKEditor plugin initialization process
+ return;
+ }
+
+ CKEDITOR.plugins.add("autosave", {
+ lang: 'ca,cs,de,en,es,fr,ja,nl,pl,pt-br,ru,sk,sv,zh,zh-cn', // %REMOVE_LINE_CORE%
+ requires: 'notification',
+ version: 0.17,
+ init: function (editor) {
+ // Default Config
+ var defaultConfig = {
+ delay: 10,
+ messageType: "notification",
+ saveDetectionSelectors: "a[href^='javascript:__doPostBack'][id*='Save'],a[id*='Cancel']",
+ saveOnDestroy: false,
+ NotOlderThen: 1440,
+ SaveKey: "autosaveKey",
+ diffType: "sideBySide"
+ };
+
+ // Get Config & Lang
+ var config = CKEDITOR.tools.extend(defaultConfig, editor.config.autosave || {}, true);
+
+ if (editor.plugins.textselection && config.messageType == "statusbar") {
+ config.messageType = "notification";
+ }
+
+ CKEDITOR.document.appendStyleSheet(CKEDITOR.getUrl(CKEDITOR.plugins.getPath('autosave') + 'css/autosave.min.css'));
+
+ editor.on('uiSpace', function(event) {
+ if (event.data.space == 'bottom' && config.messageType != null && config.messageType == "statusbar") {
+
+ event.data.html += '
tags/blocks
+ in your webpages. The ...
is one of the main basis for the styling/syntax highlighting.
+ Yet, for some systems like Drupal,
tag is blacklisted by default
+ when filtering/rendering page contents (actually
will become
when rendered).
+ In Drupal,
is allowed by default. Hence, you must allow/whitelist
tag in order for the syntax highlighter to work as intended. In Drupal, you should add the "
"" tag
+ as one of the whitelisted tags in the
+ 'Allowed HTML tags' section in 'Limit allowed HTML tags' filter/tab in
+ "admin/config/content/formats/filtered_html" page.
+
+ B. VIEWING THE PAGE
+
+ This syntax highlighter utilizes the Prism JS/CSS files in order to work as intended.
+
+ Typically, there are 2 view modes for every page in CMS: CKEditor Mode and Page Mode.
+ In CKEditor Mode (when editing the page), the Prism Highlighter handles the auto-loading
+ of Prism JS and CSS files. In Page Mode (when rendering/viewing the actual page),
+ Prism Highlighiter is not loaded anymore (since it depends on CKEditor.)
+
+ In effect, you have to load the Prism JS and CSS files when displaying the page.
+ You should add these link and script tags in your HTML :
+
+ ...
+
+
+
+
+
+ For example, in my personal site I have these values:
+
+ ...
+
+
+
+
+
+ Note that even if you're not using Prism Syntax Highlighter, and just using
+ the Code Snippet plugin, you'll have to do the same steps:
+ http://docs.ckeditor.com/#!/guide/dev_codesnippet.
+
+ In Drupal, you could easily add the script using JS Injector module
+ https://www.drupal.org/project/js_injector
+
+ and for adding the stylesheet, you could use the CSS Injector module
+ https://www.drupal.org/project/css_injector
+
+ If you're a Drupal developer, you could do the JS/CSS insertions using the
+ drupal_add_js() and drupal_add_css() functions in a custom module.
diff --git a/static/ckeditor/plugins/prism/LICENSE.txt b/static/ckeditor/plugins/prism/LICENSE.txt
new file mode 100644
index 00000000..4362b491
--- /dev/null
+++ b/static/ckeditor/plugins/prism/LICENSE.txt
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+Prism Highlighter is a plugin for CKEditor for inserting beautiful formatted text, markdown, or code snippets in your blog or website. Prism is the chosen renderer/highlighter/colorizer since it's pretty, lightweight, and extendable. I also use this plugin in my [personal website](http://www.ranelpadon.com/content/practical-regex-part-12-common-operators).
+
+This plugin utilizes the following libraries:
+
+
+
+By default, Prism has no line numbering mechanism, so the **Line Number** add-on has been added. Also, in order for the line numbers to work smoothly in CKEditor and when rendering the actual page, I did some minor patching in **prism.js** and **prism.css**.
+
+LIVE DEMO PAGE:
+Demo page could be found here.
+
+INSTALLATION:
+Kindly refer to Installation Guide.
+
+HOW TO USE:
+Kindly refer to How to Create and Edit Prism Snippets.
+
+LICENSE and CREDITS:
+License: LGPLv2.1 or later should apply. Note that LGPLv2.1+ is also compatible with GPLv2.
+Copyright 2015 by [Engr. Ranel O. Padon](http://www.ranelpadon.com)
+
+Thanks to CKEditor, Prism, and other great open-source softwares and their unsung developers.
+
+=======================================================
+
+
+
+
+
+
+
diff --git a/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css
new file mode 100644
index 00000000..a8a001fe
--- /dev/null
+++ b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.css
@@ -0,0 +1 @@
+code[class*="language-"],pre{color:white;text-shadow:0 -.1em .2em black;font-family:Monaco,Consolas,'Andale Mono','Ubuntu Mono',monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*="language-"],pre{text-shadow:none}}pre,:not(pre)>code[class*="language-"]{background:hsl(30,20%,25%)}pre{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid hsl(30,20%,40%);border-radius:.5em;box-shadow:1px 1px .5em black inset}:not(pre)>code[class*="language-"]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid hsl(30,20%,40%);box-shadow:1px 1px .3em -.1em black inset;white-space:normal}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:hsl(30,20%,50%)}.token.punctuation{opacity:.7}.namespace{opacity:.7}.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol{color:hsl(350,40%,70%)}.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{color:hsl(75,70%,60%)}.token.operator,.token.entity,.token.url,.language-css .token.string,.style .token.string,.token.variable{color:hsl(40,90%,60%)}.token.atrule,.token.attr-value,.token.keyword{color:hsl(350,40%,70%)}.token.regex,.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}pre{position:relative;padding-left:3.8em;counter-reset:linenumber}pre>code{position:relative}pre>code .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
diff --git a/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js
new file mode 100644
index 00000000..c81aa615
--- /dev/null
+++ b/static/ckeditor/plugins/prism/lib/prism/prism_patched.min.js
@@ -0,0 +1,4 @@
+var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof i?new i(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(f instanceof a)){d.lastIndex=0;var b=d.exec(f);if(b){u&&(m=b[1].length);var h=b.index-1+m,b=b[0].slice(m),S=b.length,T=h+S,A=f.slice(0,h+1),N=f.slice(T+1),I=[g,1];A&&I.push(A);var L=new a(o,p?t.tokenize(b,p):b,E);I.push(L),N&&I.push(N),Array.prototype.splice.apply(r,I)}}}}}return r},hooks:{all:{},add:function(e,i){var n=t.hooks.all;n[e]=n[e]||[],n[e].push(i)},run:function(e,i){var n=t.hooks.all[e];if(n&&n.length)for(var a,r=0;a=n[r++];)a(i)}}},i=t.Token=function(e,t,i){this.type=e,this.content=t,this.alias=i};if(i.stringify=function(e,n,a){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return i.stringify(t,n,e)}).join("");var r={type:e.type,content:i.stringify(e.content,n,a),tag:"span",classes:["token",e.type],attributes:{},language:n,parent:a};if("comment"==r.type&&(r.attributes.spellcheck="true"),e.alias){var s="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(r.classes,s)}t.hooks.run("wrap",r);var o="";for(var l in r.attributes)o+=(o?" ":"")+l+'="'+(r.attributes[l]||"")+'"';return"<"+r.tag+' class="'+r.classes.join(" ")+'" '+o+">"+r.content+""+r.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var i=JSON.parse(e.data),n=i.language,a=i.code,r=i.immediateClose;_self.postMessage(t.highlight(a,t.languages[n],n)),r&&_self.close()},!1),_self.Prism):_self.Prism;var n=document.getElementsByTagName("script");return n=n[n.length-1],n&&(t.filename=n.src,document.addEventListener&&!n.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=.$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/(");t++;var s=e(this),o,u=s.data("repo"),a=u.split("/")[0],f=u.split("/")[1],l="http://github.com/"+a,c="http://github.com/"+a+"/"+f;o=e(''],h=0;d>h;h++){g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
\ No newline at end of file
diff --git a/static/dist/js/sg_libs.min.js b/static/dist/js/sg_libs.min.js
new file mode 100644
index 00000000..1201f5bc
--- /dev/null
+++ b/static/dist/js/sg_libs.min.js
@@ -0,0 +1 @@
+var emojis=["bowtie","smile","laughing","blush","smiley","relaxed","smirk","heart_eyes","kissing_heart","kissing_closed_eyes","flushed","relieved","satisfied","grin","wink","stuck_out_tongue_winking_eye","stuck_out_tongue_closed_eyes","grinning","kissing","kissing_smiling_eyes","stuck_out_tongue","sleeping","worried","frowning","anguished","open_mouth","grimacing","confused","hushed","expressionless","unamused","sweat_smile","sweat","disappointed_relieved","weary","pensive","disappointed","confounded","fearful","cold_sweat","persevere","cry","sob","joy","astonished","scream","neckbeard","tired_face","angry","rage","triumph","sleepy","yum","mask","sunglasses","dizzy_face","imp","smiling_imp","neutral_face","no_mouth","innocent","alien","yellow_heart","blue_heart","purple_heart","heart","green_heart","broken_heart","heartbeat","heartpulse","two_hearts","revolving_hearts","cupid","sparkling_heart","sparkles","star","star2","dizzy","boom","collision","anger","exclamation","question","grey_exclamation","grey_question","zzz","dash","sweat_drops","notes","musical_note","fire","hankey","poop","shit","+1","thumbsup","-1","thumbsdown","ok_hand","punch","facepunch","fist","v","wave","hand","raised_hand","open_hands","point_up","point_down","point_left","point_right","raised_hands","pray","point_up_2","clap","muscle","metal","fu","walking","runner","running","couple","family","two_men_holding_hands","two_women_holding_hands","dancer","dancers","ok_woman","no_good","information_desk_person","raising_hand","bride_with_veil","person_with_pouting_face","person_frowning","bow","couplekiss","couple_with_heart","massage","haircut","nail_care","boy","girl","woman","man","baby","older_woman","older_man","person_with_blond_hair","man_with_gua_pi_mao","man_with_turban","construction_worker","cop","angel","princess","smiley_cat","smile_cat","heart_eyes_cat","kissing_cat","smirk_cat","scream_cat","crying_cat_face","joy_cat","pouting_cat","japanese_ogre","japanese_goblin","see_no_evil","hear_no_evil","speak_no_evil","guardsman","skull","feet","lips","kiss","droplet","ear","eyes","nose","tongue","love_letter","bust_in_silhouette","busts_in_silhouette","speech_balloon","thought_balloon","feelsgood","finnadie","goberserk","godmode","hurtrealbad","rage1","rage2","rage3","rage4","suspect","trollface","sunny","umbrella","cloud","snowflake","snowman","zap","cyclone","foggy","ocean","cat","dog","mouse","hamster","rabbit","wolf","frog","tiger","koala","bear","pig","pig_nose","cow","boar","monkey_face","monkey","horse","racehorse","camel","sheep","elephant","panda_face","snake","bird","baby_chick","hatched_chick","hatching_chick","chicken","penguin","turtle","bug","honeybee","ant","beetle","snail","octopus","tropical_fish","fish","whale","whale2","dolphin","cow2","ram","rat","water_buffalo","tiger2","rabbit2","dragon","goat","rooster","dog2","pig2","mouse2","ox","dragon_face","blowfish","crocodile","dromedary_camel","leopard","cat2","poodle","paw_prints","bouquet","cherry_blossom","tulip","four_leaf_clover","rose","sunflower","hibiscus","maple_leaf","leaves","fallen_leaf","herb","mushroom","cactus","palm_tree","evergreen_tree","deciduous_tree","chestnut","seedling","blossom","ear_of_rice","shell","globe_with_meridians","sun_with_face","full_moon_with_face","new_moon_with_face","new_moon","waxing_crescent_moon","first_quarter_moon","waxing_gibbous_moon","full_moon","waning_gibbous_moon","last_quarter_moon","waning_crescent_moon","last_quarter_moon_with_face","first_quarter_moon_with_face","moon","earth_africa","earth_americas","earth_asia","volcano","milky_way","partly_sunny","octocat","squirrel","bamboo","gift_heart","dolls","school_satchel","mortar_board","flags","fireworks","sparkler","wind_chime","rice_scene","jack_o_lantern","ghost","santa","christmas_tree","gift","bell","no_bell","tanabata_tree","tada","confetti_ball","balloon","crystal_ball","cd","dvd","floppy_disk","camera","video_camera","movie_camera","computer","tv","iphone","phone","telephone","telephone_receiver","pager","fax","minidisc","vhs","sound","speaker","mute","loudspeaker","mega","hourglass","hourglass_flowing_sand","alarm_clock","watch","radio","satellite","loop","mag","mag_right","unlock","lock","lock_with_ink_pen","closed_lock_with_key","key","bulb","flashlight","high_brightness","low_brightness","electric_plug","battery","calling","email","mailbox","postbox","bath","bathtub","shower","toilet","wrench","nut_and_bolt","hammer","seat","moneybag","yen","dollar","pound","euro","credit_card","money_with_wings","e-mail","inbox_tray","outbox_tray","envelope","incoming_envelope","postal_horn","mailbox_closed","mailbox_with_mail","mailbox_with_no_mail","package","door","smoking","bomb","gun","hocho","pill","syringe","page_facing_up","page_with_curl","bookmark_tabs","bar_chart","chart_with_upwards_trend","chart_with_downwards_trend","scroll","clipboard","calendar","date","card_index","file_folder","open_file_folder","scissors","pushpin","paperclip","black_nib","pencil2","straight_ruler","triangular_ruler","closed_book","green_book","blue_book","orange_book","notebook","notebook_with_decorative_cover","ledger","books","bookmark","name_badge","microscope","telescope","newspaper","football","basketball","soccer","baseball","tennis","8ball","rugby_football","bowling","golf","mountain_bicyclist","bicyclist","horse_racing","snowboarder","swimmer","surfer","ski","spades","hearts","clubs","diamonds","gem","ring","trophy","musical_score","musical_keyboard","violin","space_invader","video_game","black_joker","flower_playing_cards","game_die","dart","mahjong","clapper","memo","pencil","book","art","microphone","headphones","trumpet","saxophone","guitar","shoe","sandal","high_heel","lipstick","boot","shirt","tshirt","necktie","womans_clothes","dress","running_shirt_with_sash","jeans","kimono","bikini","ribbon","tophat","crown","womans_hat","mans_shoe","closed_umbrella","briefcase","handbag","pouch","purse","eyeglasses","fishing_pole_and_fish","coffee","tea","sake","baby_bottle","beer","beers","cocktail","tropical_drink","wine_glass","fork_and_knife","pizza","hamburger","fries","poultry_leg","meat_on_bone","spaghetti","curry","fried_shrimp","bento","sushi","fish_cake","rice_ball","rice_cracker","rice","ramen","stew","oden","dango","egg","bread","doughnut","custard","icecream","ice_cream","shaved_ice","birthday","cake","cookie","chocolate_bar","candy","lollipop","honey_pot","apple","green_apple","tangerine","lemon","cherries","grapes","watermelon","strawberry","peach","melon","banana","pear","pineapple","sweet_potato","eggplant","tomato","corn"];function md5cycle(t,e){var i=ff(i=t[0],r=t[1],n=t[2],o=t[3],e[0],7,-680876936),o=ff(o,i,r,n,e[1],12,-389564586),n=ff(n,o,i,r,e[2],17,606105819),r=ff(r,n,o,i,e[3],22,-1044525330);i=ff(i,r,n,o,e[4],7,-176418897),o=ff(o,i,r,n,e[5],12,1200080426),n=ff(n,o,i,r,e[6],17,-1473231341),r=ff(r,n,o,i,e[7],22,-45705983),i=ff(i,r,n,o,e[8],7,1770035416),o=ff(o,i,r,n,e[9],12,-1958414417),n=ff(n,o,i,r,e[10],17,-42063),r=ff(r,n,o,i,e[11],22,-1990404162),i=ff(i,r,n,o,e[12],7,1804603682),o=ff(o,i,r,n,e[13],12,-40341101),n=ff(n,o,i,r,e[14],17,-1502002290),i=gg(i,r=ff(r,n,o,i,e[15],22,1236535329),n,o,e[1],5,-165796510),o=gg(o,i,r,n,e[6],9,-1069501632),n=gg(n,o,i,r,e[11],14,643717713),r=gg(r,n,o,i,e[0],20,-373897302),i=gg(i,r,n,o,e[5],5,-701558691),o=gg(o,i,r,n,e[10],9,38016083),n=gg(n,o,i,r,e[15],14,-660478335),r=gg(r,n,o,i,e[4],20,-405537848),i=gg(i,r,n,o,e[9],5,568446438),o=gg(o,i,r,n,e[14],9,-1019803690),n=gg(n,o,i,r,e[3],14,-187363961),r=gg(r,n,o,i,e[8],20,1163531501),i=gg(i,r,n,o,e[13],5,-1444681467),o=gg(o,i,r,n,e[2],9,-51403784),n=gg(n,o,i,r,e[7],14,1735328473),i=hh(i,r=gg(r,n,o,i,e[12],20,-1926607734),n,o,e[5],4,-378558),o=hh(o,i,r,n,e[8],11,-2022574463),n=hh(n,o,i,r,e[11],16,1839030562),r=hh(r,n,o,i,e[14],23,-35309556),i=hh(i,r,n,o,e[1],4,-1530992060),o=hh(o,i,r,n,e[4],11,1272893353),n=hh(n,o,i,r,e[7],16,-155497632),r=hh(r,n,o,i,e[10],23,-1094730640),i=hh(i,r,n,o,e[13],4,681279174),o=hh(o,i,r,n,e[0],11,-358537222),n=hh(n,o,i,r,e[3],16,-722521979),r=hh(r,n,o,i,e[6],23,76029189),i=hh(i,r,n,o,e[9],4,-640364487),o=hh(o,i,r,n,e[12],11,-421815835),n=hh(n,o,i,r,e[15],16,530742520),i=ii(i,r=hh(r,n,o,i,e[2],23,-995338651),n,o,e[0],6,-198630844),o=ii(o,i,r,n,e[7],10,1126891415),n=ii(n,o,i,r,e[14],15,-1416354905),r=ii(r,n,o,i,e[5],21,-57434055),i=ii(i,r,n,o,e[12],6,1700485571),o=ii(o,i,r,n,e[3],10,-1894986606),n=ii(n,o,i,r,e[10],15,-1051523),r=ii(r,n,o,i,e[1],21,-2054922799),i=ii(i,r,n,o,e[8],6,1873313359),o=ii(o,i,r,n,e[15],10,-30611744),n=ii(n,o,i,r,e[6],15,-1560198380),r=ii(r,n,o,i,e[13],21,1309151649),i=ii(i,r,n,o,e[4],6,-145523070),o=ii(o,i,r,n,e[11],10,-1120210379),n=ii(n,o,i,r,e[2],15,718787259),r=ii(r,n,o,i,e[9],21,-343485551),t[0]=add32(i,t[0]),t[1]=add32(r,t[1]),t[2]=add32(n,t[2]),t[3]=add32(o,t[3])}function cmn(t,e,i,o,n,r){return e=add32(add32(e,t),add32(o,r)),add32(e<");for(var i=0;d>i;i++)g.push(' ")}g.push("');g.push(" "),this.timeout_id=null,this.context.$el.append(this.$el),this.bind_event()}function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}function c(t,e){this.app=t,this.at=e,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.setting=null,this.query=null,this.pos=0,this.cur_rect=null,this.range=null,0===(this.$el=s("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=s("")),this.model=new l(this),this.view=new h(this)}function d(t){this.current_flag=null,this.controllers={},this.alias_maps={},this.$inputor=s(t),this.setIframe(),this.listen()}d.prototype.createContainer=function(t){return 0===(this.$el=s("#atwho-container",t)).length?s(t.body).append(this.$el=s("")):void 0},d.prototype.setIframe=function(t,e){return null==e&&(e=!1),t?(this.window=t.contentWindow,this.document=t.contentDocument||this.window.document,this.iframe=t):(this.document=document,this.window=window,this.iframe=null),(this.iframeStandalone=e)?(null!=(e=this.$el)&&e.remove(),this.createContainer(this.document)):this.createContainer(document)},d.prototype.controller=function(t){var e,i,o,n;if(this.alias_maps[t])i=this.controllers[this.alias_maps[t]];else for(o in n=this.controllers)if(e=n[o],o===t){i=e;break}return i||this.controllers[this.current_flag]},d.prototype.set_context_for=function(t){return this.current_flag=t,this},d.prototype.reg=function(t,e){var i=(i=this.controllers)[t]||(i[t]=new o(this,t));return e.alias&&(this.alias_maps[e.alias]=t),i.init(e),this},d.prototype.listen=function(){return this.$inputor.on("keyup.atwhoInner",(r=this,function(t){return r.on_keyup(t)})).on("keydown.atwhoInner",(e=this,function(t){return e.on_keydown(t)})).on("scroll.atwhoInner",(n=this,function(t){var e;return null!=(e=n.controller())?e.view.hide(t):void 0})).on("blur.atwhoInner",(o=this,function(t){var e;return(e=o.controller())?e.view.hide(t,e.get_opt("display_timeout")):void 0})).on("click.atwhoInner",(i=this,function(t){var e;return null!=(e=i.controller())?e.view.hide(t):void 0}));var i,o,n,e,r},d.prototype.shutdown=function(){var t,e,i=this.controllers;for(e in i)t=i[e],t.destroy(),delete this.controllers[e];return this.$inputor.off(".atwhoInner"),this.$el.remove()},d.prototype.dispatch=function(){return s.map(this.controllers,(i=this,function(t){var e;return(e=t.get_opt("delay"))?(clearTimeout(i.delayedCallback),i.delayedCallback=setTimeout(function(){return t.look_up()?i.set_context_for(t.at):void 0},e)):t.look_up()?i.set_context_for(t.at):void 0}));var i},d.prototype.on_keyup=function(t){var e;switch(t.keyCode){case n.ESC:t.preventDefault(),null!=(e=this.controller())&&e.view.hide();break;case n.DOWN:case n.UP:case n.CTRL:s.noop();break;case n.P:case n.N:t.ctrlKey||this.dispatch();break;default:this.dispatch()}},d.prototype.on_keydown=function(t){var e,i=null!=(e=this.controller())?e.view:void 0;if(i&&i.visible())switch(t.keyCode){case n.ESC:t.preventDefault(),i.hide(t);break;case n.UP:t.preventDefault(),i.prev();break;case n.DOWN:t.preventDefault(),i.next();break;case n.P:if(!t.ctrlKey)return;t.preventDefault(),i.prev();break;case n.N:if(!t.ctrlKey)return;t.preventDefault(),i.next();break;case n.TAB:case n.ENTER:if(!i.visible())return;t.preventDefault(),i.choose(t);break;default:s.noop()}},a=d,c.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},c.prototype.init=function(t){return this.setting=s.extend({},this.setting||s.fn.atwho.default,t),this.view.init(),this.model.reload(this.setting.data)},c.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},c.prototype.call_default=function(){var e=arguments[0],t=2<=arguments.length?u.call(arguments,1):[];try{return i[e].apply(this,t)}catch(t){return s.error(t+" Or maybe At.js doesn't have function "+e)}},c.prototype.trigger=function(t,e){var i;return(e=null==e?[]:e).push(this),t=(i=this.get_opt("alias"))?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(t,e)},c.prototype.callbacks=function(t){return this.get_opt("callbacks")[t]||i[t]},c.prototype.get_opt=function(t){try{return this.setting[t]}catch(t){return null}},c.prototype.content=function(){return this.$inputor.is("textarea, input")?this.$inputor.val():this.$inputor.text()},c.prototype.catch_query=function(){var t=this.content(),e=this.$inputor.caret("pos",{iframe:this.app.iframe}),i=t.slice(0,e),t=this.callbacks("matcher").call(this,this.at,i,this.get_opt("start_with_space"));return"string"==typeof t&&t.length<=this.get_opt("max_len",20)?(e=(i=e-t.length)+t.length,t={text:t,head_pos:this.pos=i,end_pos:e},this.trigger("matched",[this.at,t.text])):(t=null,this.view.hide()),this.query=t},c.prototype.rect=function(){var t,e;if(t=this.$inputor.caret("offset",this.pos-1,{iframe:this.app.iframe}))return this.app.iframe&&!this.app.iframeStandalone&&(e=s(this.app.iframe).offset(),t.left+=e.left,t.top+=e.top),this.$inputor.is("[contentEditable]")&&(t=this.cur_rect||(this.cur_rect=t)),e=this.app.document.selection?0:2,{left:t.left,top:t.top,bottom:t.top+t.height+e}},c.prototype.reset_rect=function(){return this.$inputor.is("[contentEditable]")?this.cur_rect=null:void 0},c.prototype.mark_range=function(){var t;if(this.$inputor.is("[contentEditable]"))return this.app.window.getSelection&&0<(t=this.app.window.getSelection()).rangeCount?this.range=t.getRangeAt(0):this.app.document.selection?this.ie8_range=this.app.document.selection.createRange():void 0},c.prototype.insert_content_for=function(t){var e=t.data("value"),i=this.get_opt("insert_tpl");return this.$inputor.is("textarea, input")||!i?e:(e=s.extend({},t.data("item-data"),{"atwho-data-value":e,"atwho-at":this.at}),this.callbacks("tpl_eval").call(this,i,e))},c.prototype.insert=function(t){var e,i,o=this.$inputor,t=this.callbacks("inserting_wrapper").call(this,o,t,this.get_opt("suffix"));return o.is("textarea, input")?(i=""+(e=(i=o.val()).slice(0,Math.max(this.query.head_pos-this.at.length,0)))+t+i.slice(this.query.end_pos||0),o.val(i),o.caret("pos",e.length+t.length,{iframe:this.app.iframe})):(i=this.range)?(e=i.startOffset-(this.query.end_pos-this.query.head_pos)-this.at.length,i.setStart(i.endContainer,Math.max(e,0)),i.setEnd(i.endContainer,i.endOffset),i.deleteContents(),e=s(t,this.app.document)[0],i.insertNode(e),i.setEndAfter(e),i.collapse(!1),(e=this.app.window.getSelection()).removeAllRanges(),e.addRange(i)):(i=this.ie8_range)&&(i.moveStart("character",this.query.end_pos-this.query.head_pos-this.at.length),i.pasteHTML(t),i.collapse(!1),i.select()),o.is(":focus")||o.focus(),o.change()},c.prototype.render_view=function(t){var e=this.get_opt("search_key");return t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.get_opt("limit")))},c.prototype.look_up=function(){var t,e;if(t=this.catch_query())return e=function(t){return t&&0
")+".
.",jQuery(t).height()!=jQuery(i).height()&&jQuery(t).height(jQuery(i).height())}var e,i=(e=this,jQuery(e).after(''),jQuery(e).next(".autogrow-textarea-mirror")[0]);i.style.display="none",i.style.wordWrap="break-word",i.style.padding=jQuery(this).css("padding"),i.style.width=jQuery(this).css("width"),i.style.fontFamily=jQuery(this).css("font-family"),i.style.fontSize=jQuery(this).css("font-size"),i.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=function(){t(this)},t(this)})},function(o){o.fn.cftoaster=function(t){var e=o.extend({},o.fn.cftoaster.options,t);return this.each(function(){e.element=o(this),!function(t){for(var e="",i=0;i<=o.cftoaster.DESTROY_COMMAND.length&&t.hasOwnProperty(i);i++)e+=t[i];return e==o.cftoaster.DESTROY_COMMAND}(e)?o.cftoaster._addToQueue(e):o.cftoaster._destroy(e)})},o.fn.cftoaster.options={content:"This is a toast message eh",element:"body",animationTime:150,showTime:3e3,maxWidth:250,backgroundColor:"#1a1a1a",fontColor:"#eaeaea",bottomMargin:75}}(jQuery),jQuery.extend({cftoaster:{NAMESPACE:"cf_toaster",DESTROY_COMMAND:"destroy",MAIN_CSS_CLASS:"cf_toaster",_queue:[],_addToQueue:function(t){this._queue.push(t),t.element&&!this._isShowingToastMessage(t.element)&&this._showNextInQueue(t.element)},_removeFromQueue:function(t){if(t)for(var e in this._queue){var i=this._queue[e];$(i.element).is(t)&&this._queue.splice(e,1)}else this._queue=[]},_destroy:function(t){t=t&&t.element?t.element:void 0;(t?$(t).find("."+this.MAIN_CSS_CLASS):$("."+this.MAIN_CSS_CLASS)).remove(),this._removeFromQueue(t)},_isShowingToastMessage:function(t){var e=!1;return e=t?0<$(t).find("."+this.MAIN_CSS_CLASS).size():e},_showNextInQueue:function(t){for(var e,i,o,n=0;n':""+l+"";s+="
抱歉!页面无法访问……
+
+
+ 数据列表
{{end}}
{{define "js"}}
-
+
+
+
-
-
+
-
+
{{end}}
diff --git a/websites/code/studygolang/template/admin/article/new.html b/template/admin/article/new.html
similarity index 79%
rename from websites/code/studygolang/template/admin/article/new.html
rename to template/admin/article/new.html
index 93414176..a1e788ae 100644
--- a/websites/code/studygolang/template/admin/article/new.html
+++ b/template/admin/article/new.html
@@ -21,7 +21,7 @@ 抓取文章
{{end}}
{{define "js"}}
-
-
+
+
{{end}}
\ No newline at end of file
diff --git a/template/admin/article/publish.html b/template/admin/article/publish.html
new file mode 100644
index 00000000..816fc06c
--- /dev/null
+++ b/template/admin/article/publish.html
@@ -0,0 +1,124 @@
+{{define "content"}}
+发布文章
+
+
+
+
+总数:{{ .total }}
{{ .Ctime }}
修改
+ 放入主题
数据列表
{{end}}
{{define "js"}}
-
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+{{template "js" .}}
+
+