internal/controller/template_controller.go (593 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package controller import ( "encoding/json" "fmt" "html/template" "net/http" "net/url" "regexp" "strings" "time" "github.com/apache/answer/internal/base/middleware" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/service/content" "github.com/apache/answer/internal/service/event_queue" "github.com/apache/answer/plugin" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" templaterender "github.com/apache/answer/internal/controller/template_render" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/siteinfo_common" "github.com/apache/answer/pkg/checker" "github.com/apache/answer/pkg/converter" "github.com/apache/answer/pkg/htmltext" "github.com/apache/answer/pkg/obj" "github.com/apache/answer/pkg/uid" "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) var SiteUrl = "" type TemplateController struct { scriptPath []string cssPath string templateRenderController *templaterender.TemplateRenderController siteInfoService siteinfo_common.SiteInfoCommonService eventQueueService event_queue.EventQueueService userService *content.UserService questionService *content.QuestionService } // NewTemplateController new controller func NewTemplateController( templateRenderController *templaterender.TemplateRenderController, siteInfoService siteinfo_common.SiteInfoCommonService, eventQueueService event_queue.EventQueueService, userService *content.UserService, questionService *content.QuestionService, ) *TemplateController { script, css := GetStyle() return &TemplateController{ scriptPath: script, cssPath: css, templateRenderController: templateRenderController, siteInfoService: siteInfoService, eventQueueService: eventQueueService, userService: userService, questionService: questionService, } } func GetStyle() (script []string, css string) { file, err := ui.Build.ReadFile("build/index.html") if err != nil { return } scriptRegexp := regexp.MustCompile(`<script defer="defer" src="([^"]*)"></script>`) scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1) for _, s := range scriptData { if len(s) == 2 { script = append(script, s[1]) } } cssRegexp := regexp.MustCompile(`<link href="(.*)" rel="stylesheet">`) cssListData := cssRegexp.FindStringSubmatch(string(file)) if len(cssListData) == 2 { css = cssListData[1] } return } func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp { var err error resp := &schema.TemplateSiteInfoResp{} resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx) if err != nil { log.Error(err) } SiteUrl = resp.General.SiteUrl resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx) if err != nil { log.Error(err) } resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx) if err != nil { log.Error(err) } resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx) if err != nil { log.Error(err) } resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx) if err != nil { log.Error(err) } resp.Year = fmt.Sprintf("%d", time.Now().Year()) return resp } // Index question list func (tc *TemplateController) Index(ctx *gin.Context) { req := &schema.QuestionPageReq{ OrderCond: "newest", } if handler.BindAndCheck(ctx, req) { return } var page = req.Page data, count, err := tc.templateRenderController.Index(ctx, req) if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { tc.Page404(ctx) return } hotQuestionReq := &schema.QuestionPageReq{ Page: 1, PageSize: 6, OrderCond: "hot", InDays: 7, } hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = siteInfo.General.SiteUrl UrlUseTitle := false if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { UrlUseTitle = true } siteInfo.Title = "" tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ "data": data, "useTitle": UrlUseTitle, "page": templaterender.Paginator(page, req.PageSize, count), "path": "questions", "hotQuestion": hotQuestion, }) } func (tc *TemplateController) QuestionList(ctx *gin.Context) { req := &schema.QuestionPageReq{ OrderCond: "newest", } if handler.BindAndCheck(ctx, req) { return } var page = req.Page data, count, err := tc.templateRenderController.Index(ctx, req) if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { tc.Page404(ctx) return } hotQuestionReq := &schema.QuestionPageReq{ Page: 1, PageSize: 6, OrderCond: "hot", InDays: 7, } hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl) if page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page) } UrlUseTitle := false if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { UrlUseTitle = true } siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLang(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ "data": data, "useTitle": UrlUseTitle, "page": templaterender.Paginator(page, req.PageSize, count), "hotQuestion": hotQuestion, }) } func (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) { questionID := ctx.Param("id") title := ctx.Param("title") answerID := uid.DeShortID(title) titleIsAnswerID := false needChangeShortID := false objectType, err := obj.GetObjectTypeStrByObjectID(answerID) if err == nil && objectType == constant.AnswerObjectType { titleIsAnswerID = true } siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx) if err != nil { return false, "" } isShortID := uid.IsShortID(questionID) if siteSeo.IsShortLink() { if !isShortID { questionID = uid.EnShortID(questionID) needChangeShortID = true } if titleIsAnswerID { answerID = uid.EnShortID(answerID) } } else { if isShortID { needChangeShortID = true questionID = uid.DeShortID(questionID) } if titleIsAnswerID { answerID = uid.DeShortID(answerID) } } if _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil { answerID = "" titleIsAnswerID = false } url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, questionID) if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { if len(ctx.Request.URL.Query()) > 0 { url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) } if needChangeShortID { return true, url } //not have title if titleIsAnswerID || len(title) == 0 { return false, "" } return true, url } else { detail, err := tc.templateRenderController.QuestionDetail(ctx, questionID) if err != nil { tc.Page404(ctx) return } url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title)) if titleIsAnswerID { url = fmt.Sprintf("%s/%s", url, answerID) } if len(ctx.Request.URL.Query()) > 0 { url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) } //have title if len(title) > 0 && !titleIsAnswerID && correctTitle { if needChangeShortID { return true, url } return false, "" } return true, url } } // QuestionInfo question and answers info func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { id := ctx.Param("id") title := ctx.Param("title") answerid := ctx.Param("answerid") shareUsername := ctx.Query("share") if checker.IsQuestionsIgnorePath(id) { // if id == "ask" { file, err := ui.Build.ReadFile("build/index.html") if err != nil { log.Error(err) tc.Page404(ctx) return } ctx.Header("content-type", "text/html;charset=utf-8") ctx.String(http.StatusOK, string(file)) return } correctTitle := false detail, err := tc.templateRenderController.QuestionDetail(ctx, id) if err != nil { tc.Page404(ctx) return } if len(shareUsername) > 0 { userInfo, err := tc.userService.GetOtherUserInfoByUsername( ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername}) if err == nil { tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID). QID(id, detail.UserID).AID(answerid, "")) } } encodeTitle := htmltext.UrlTitle(detail.Title) if encodeTitle == title { correctTitle = true } siteInfo := tc.SiteInfo(ctx) jump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle) if jump { ctx.Redirect(http.StatusFound, jumpurl) return } // answers answerReq := &schema.AnswerListReq{ QuestionID: id, Order: "", Page: 1, PageSize: 999, UserID: "", } answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq) if err != nil { tc.Page404(ctx) return } // comments objectIDs := []string{uid.DeShortID(id)} for _, answer := range answers { answerID := uid.DeShortID(answer.ID) objectIDs = append(objectIDs, answerID) } comments, err := tc.templateRenderController.CommentList(ctx, objectIDs) if err != nil { tc.Page404(ctx) return } UrlUseTitle := false if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { UrlUseTitle = true } //related question userID := middleware.GetLoginUserIDFromContext(ctx) relatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID) siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle) if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id) } jsonLD := &schema.QAPageJsonLD{} jsonLD.Context = "https://schema.org" jsonLD.Type = "QAPage" jsonLD.MainEntity.Type = "Question" jsonLD.MainEntity.Name = detail.Title jsonLD.MainEntity.Text = detail.HTML jsonLD.MainEntity.AnswerCount = int(answerCount) jsonLD.MainEntity.UpvoteCount = detail.VoteCount jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0) jsonLD.MainEntity.Author.Type = "Person" jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName jsonLD.MainEntity.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, detail.UserInfo.Username) answerList := make([]*schema.SuggestedAnswerItem, 0) for _, answer := range answers { if answer.Accepted == schema.AnswerAcceptedEnable { acceptedAnswerItem := &schema.AcceptedAnswerItem{} acceptedAnswerItem.Type = "Answer" acceptedAnswerItem.Text = answer.HTML acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0) acceptedAnswerItem.UpvoteCount = answer.VoteCount acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) acceptedAnswerItem.Author.Type = "Person" acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName acceptedAnswerItem.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem } else { item := &schema.SuggestedAnswerItem{} item.Type = "Answer" item.Text = answer.HTML item.DateCreated = time.Unix(answer.CreateTime, 0) item.UpvoteCount = answer.VoteCount item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) item.Author.Type = "Person" item.Author.Name = answer.UserInfo.DisplayName item.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) answerList = append(answerList, item) } } jsonLD.MainEntity.SuggestedAnswer = answerList jsonLDStr, err := json.Marshal(jsonLD) if err == nil { siteInfo.JsonLD = `<script data-react-helmet="true" type="application/ld+json">` + string(jsonLDStr) + ` </script>` } siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240) tags := make([]string, 0) for _, tag := range detail.Tags { tags = append(tags, tag.DisplayName) } siteInfo.Keywords = strings.Replace(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",", -1) siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name) tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{ "id": id, "answerid": answerid, "detail": detail, "answers": answers, "comments": comments, "noindex": detail.Show == entity.QuestionHide, "useTitle": UrlUseTitle, "relatedQuestion": relatedQuestion, }) } // TagList tags list func (tc *TemplateController) TagList(ctx *gin.Context) { req := &schema.GetTagWithPageReq{ PageSize: constant.DefaultPageSize, Page: 1, } if handler.BindAndCheck(ctx, req) { return } data, err := tc.templateRenderController.TagList(ctx, req) if err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) { tc.Page404(ctx) return } page := templaterender.Paginator(req.Page, req.PageSize, data.Count) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl) if req.Page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page) } siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLang(ctx), constant.TagsListTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{ "page": page, "data": data, }) } // TagInfo taginfo func (tc *TemplateController) TagInfo(ctx *gin.Context) { tag := ctx.Param("tag") req := &schema.GetTamplateTagInfoReq{} if handler.BindAndCheck(ctx, req) { tc.Page404(ctx) return } nowPage := req.Page req.Name = tag tagInfo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req) if err != nil { tc.Page404(ctx) return } page := templaterender.Paginator(nowPage, req.PageSize, questionCount) siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag) if req.Page > 1 { siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page) } siteInfo.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240) if len(tagInfo.ParsedText) == 0 { siteInfo.Description = translator.Tr(handler.GetLang(ctx), constant.TagHasNoDescription) } siteInfo.Keywords = tagInfo.DisplayName UrlUseTitle := false if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { UrlUseTitle = true } siteInfo.Title = fmt.Sprintf("'%s' %s - %s", tagInfo.DisplayName, translator.Tr(handler.GetLang(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{ "tag": tagInfo, "questionList": questionList, "questionCount": questionCount, "useTitle": UrlUseTitle, "page": page, }) } // UserInfo user info func (tc *TemplateController) UserInfo(ctx *gin.Context) { username := ctx.Param("username") if username == "" { tc.Page404(ctx) return } exist := checker.IsUsersIgnorePath(username) if exist { file, err := ui.Build.ReadFile("build/index.html") if err != nil { log.Error(err) tc.Page404(ctx) return } ctx.Header("content-type", "text/html;charset=utf-8") ctx.String(http.StatusOK, string(file)) return } req := &schema.GetOtherUserInfoByUsernameReq{} req.Username = username userinfo, err := tc.templateRenderController.UserInfo(ctx, req) if err != nil { tc.Page404(ctx) return } questionList, answerList, err := tc.questionService.SearchUserTopList(ctx, req.Username, "") if err != nil { tc.Page404(ctx) return } siteInfo := tc.SiteInfo(ctx) siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username) siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name) tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{ "userinfo": userinfo, "bio": template.HTML(userinfo.BioHTML), "topQuestions": questionList, "topAnswers": answerList, }) } func (tc *TemplateController) Page404(ctx *gin.Context) { tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{}) } func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) { var ( prefix = "" cssPath = "" scriptPath = make([]string, len(tc.scriptPath)) ) _ = plugin.CallCDN(func(fn plugin.CDN) error { prefix = fn.GetStaticPrefix() return nil }) if prefix != "" { if prefix[len(prefix)-1:] == "/" { prefix = strings.TrimSuffix(prefix, "/") } cssPath = prefix + tc.cssPath for i, path := range tc.scriptPath { scriptPath[i] = prefix + path } } else { cssPath = tc.cssPath scriptPath = tc.scriptPath } data["siteinfo"] = siteInfo data["baseURL"] = "" if parsedUrl, err := url.Parse(siteInfo.General.SiteUrl); err == nil { data["baseURL"] = parsedUrl.Path } data["scriptPath"] = scriptPath data["cssPath"] = cssPath data["keywords"] = siteInfo.Keywords if siteInfo.Description == "" { siteInfo.Description = siteInfo.General.Description } data["title"] = siteInfo.Title if siteInfo.Title == "" { data["title"] = siteInfo.General.Name } data["description"] = siteInfo.Description data["language"] = handler.GetLang(ctx) data["timezone"] = siteInfo.Interface.TimeZone language := strings.Replace(siteInfo.Interface.Language, "_", "-", -1) data["lang"] = language data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter data["Version"] = constant.Version data["Revision"] = constant.Revision _, ok := data["path"] if !ok { data["path"] = "" } ctx.Header("X-Frame-Options", "DENY") ctx.HTML(code, tpl, data) } func (tc *TemplateController) OpenSearch(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } tc.templateRenderController.OpenSearch(ctx) } func (tc *TemplateController) Sitemap(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } tc.templateRenderController.Sitemap(ctx) } func (tc *TemplateController) SitemapPage(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) return } page := 0 pageParam := ctx.Param("page") pageRegexp := regexp.MustCompile(`question-(.*).xml`) pageStr := pageRegexp.FindStringSubmatch(pageParam) if len(pageStr) != 2 { tc.Page404(ctx) return } page = converter.StringToInt(pageStr[1]) if page == 0 { tc.Page404(ctx) return } err := tc.templateRenderController.SitemapPage(ctx, page) if err != nil { tc.Page404(ctx) return } } func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool { resp, err := tc.siteInfoService.GetSiteLogin(ctx) if err != nil { log.Error(err) return false } if resp.LoginRequired { return true } return false }