Part 1 of URL robustification

This commit is contained in:
Asaf Gartner 2021-05-05 23:34:32 +03:00
parent dca101fd20
commit 06bbc2b9cc
10 changed files with 566 additions and 85 deletions

View File

@ -18,7 +18,11 @@ type Q struct {
var baseUrlParsed url.URL
func init() {
parsed, err := url.Parse(config.Config.BaseUrl)
SetGlobalBaseUrl(config.Config.BaseUrl)
}
func SetGlobalBaseUrl(fullBaseUrl string) {
parsed, err := url.Parse(fullBaseUrl)
if err != nil {
panic(oops.New(err, "could not parse base URL"))
}

View File

@ -1,6 +1,8 @@
package hmnurl
import (
"net/url"
"regexp"
"testing"
"git.handmade.network/hmn/hmn/src/config"
@ -8,10 +10,10 @@ import (
)
func TestUrl(t *testing.T) {
defer func(original string) {
config.Config.BaseUrl = original
}(config.Config.BaseUrl)
config.Config.BaseUrl = "http://handmade.test"
defer func() {
SetGlobalBaseUrl(config.Config.BaseUrl)
}()
SetGlobalBaseUrl("http://handmade.test")
t.Run("no query", func(t *testing.T) {
result := Url("/test/foo", nil)
@ -22,3 +24,123 @@ func TestUrl(t *testing.T) {
assert.Equal(t, "http://handmade.test/test/foo?bar=baz&zig%3F%3F=zig+%26+zag%21%21", result)
})
}
func TestHomepage(t *testing.T) {
AssertRegexMatch(t, BuildHomepage(), RegexHomepage, nil)
}
func TestLogin(t *testing.T) {
AssertRegexMatch(t, BuildLogin(), RegexLogin, nil)
}
func TestLogout(t *testing.T) {
AssertRegexMatch(t, BuildLogout(), RegexLogout, nil)
}
func TestStaticPages(t *testing.T) {
AssertRegexMatch(t, BuildManifesto(), RegexManifesto, nil)
AssertRegexMatch(t, BuildAbout(), RegexAbout, nil)
AssertRegexMatch(t, BuildCodeOfConduct(), RegexCodeOfConduct, nil)
AssertRegexMatch(t, BuildCommunicationGuidelines(), RegexCommunicationGuidelines, nil)
AssertRegexMatch(t, BuildContactPage(), RegexContactPage, nil)
AssertRegexMatch(t, BuildMonthlyUpdatePolicy(), RegexMonthlyUpdatePolicy, nil)
AssertRegexMatch(t, BuildProjectSubmissionGuidelines(), RegexProjectSubmissionGuidelines, nil)
}
func TestFeed(t *testing.T) {
AssertRegexMatch(t, BuildFeed(), RegexFeed, nil)
assert.Equal(t, BuildFeed(), BuildFeedWithPage(1))
AssertRegexMatch(t, BuildFeedWithPage(1), RegexFeed, nil)
AssertRegexMatch(t, "/feed/1", RegexFeed, nil) // NOTE(asaf): We should never build this URL, but we should still accept it.
AssertRegexMatch(t, BuildFeedWithPage(5), RegexFeed, map[string]string{"page": "5"})
assert.Panics(t, func() { BuildFeedWithPage(-1) })
assert.Panics(t, func() { BuildFeedWithPage(0) })
}
func TestForumThread(t *testing.T) {
AssertRegexMatch(t, BuildForumThread("", nil, 1, 1), RegexForumThread, map[string]string{"threadid": "1"})
AssertRegexMatch(t, BuildForumThread("", []string{"wip"}, 1, 2), RegexForumThread, map[string]string{"cats": "forums/wip", "page": "2", "threadid": "1"})
AssertRegexMatch(t, BuildForumThread("", []string{"sub", "wip"}, 1, 2), RegexForumThread, map[string]string{"cats": "forums/sub/wip", "page": "2", "threadid": "1"})
AssertSubdomain(t, BuildForumThread("hmn", nil, 1, 1), "")
AssertSubdomain(t, BuildForumThread("", nil, 1, 1), "")
AssertSubdomain(t, BuildForumThread("hero", nil, 1, 1), "hero")
assert.Panics(t, func() { BuildForumThread("", []string{"", "wip"}, 1, 1) })
assert.Panics(t, func() { BuildForumThread("", []string{" ", "wip"}, 1, 1) })
assert.Panics(t, func() { BuildForumThread("", []string{"wip/jobs"}, 1, 1) })
}
func TestForumCategory(t *testing.T) {
AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil)
AssertRegexMatch(t, BuildForumCategory("", []string{"wip"}, 2), RegexForumCategory, map[string]string{"cats": "forums/wip", "page": "2"})
AssertRegexMatch(t, BuildForumCategory("", []string{"sub", "wip"}, 2), RegexForumCategory, map[string]string{"cats": "forums/sub/wip", "page": "2"})
AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "")
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
assert.Panics(t, func() { BuildForumCategory("", []string{"", "wip"}, 1) })
assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) })
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
}
func TestProjectCSS(t *testing.T) {
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
}
func TestPublic(t *testing.T) {
AssertRegexMatch(t, BuildPublic("test"), RegexPublic, nil)
AssertRegexMatch(t, BuildPublic("/test"), RegexPublic, nil)
AssertRegexMatch(t, BuildPublic("/test/"), RegexPublic, nil)
AssertRegexMatch(t, BuildPublic("/test/thing/image.png"), RegexPublic, nil)
assert.Panics(t, func() { BuildPublic("") })
assert.Panics(t, func() { BuildPublic("/") })
assert.Panics(t, func() { BuildPublic("/thing//image.png") })
assert.Panics(t, func() { BuildPublic("/thing/ /image.png") })
}
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
parsed, err := url.Parse(fullUrl)
ok := assert.Nilf(t, err, "Full url could not be parsed: %s", fullUrl)
if !ok {
return
}
fullHost := parsed.Host
if len(expectedSubdomain) == 0 {
assert.Equal(t, baseUrlParsed.Host, fullHost, "Did not expect a subdomain")
} else {
assert.Equalf(t, expectedSubdomain+"."+baseUrlParsed.Host, fullHost, "Subdomain mismatch")
}
}
func AssertRegexMatch(t *testing.T, fullUrl string, regex *regexp.Regexp, paramsToVerify map[string]string) {
parsed, err := url.Parse(fullUrl)
ok := assert.Nilf(t, err, "Full url could not be parsed: %s", fullUrl)
if !ok {
return
}
requestPath := parsed.Path
if len(requestPath) == 0 {
requestPath = "/"
}
match := regex.FindStringSubmatch(requestPath)
assert.NotNilf(t, match, "Url did not match regex: [%s] vs [%s]", requestPath, regex.String())
if paramsToVerify != nil {
subexpNames := regex.SubexpNames()
for i, matchedValue := range match {
paramName := subexpNames[i]
expectedValue, ok := paramsToVerify[paramName]
if ok {
assert.Equalf(t, expectedValue, matchedValue, "Param mismatch for [%s]", paramName)
delete(paramsToVerify, paramName)
}
}
if len(paramsToVerify) > 0 {
unmatchedParams := make([]string, 0, len(paramsToVerify))
for paramName := range paramsToVerify {
unmatchedParams = append(unmatchedParams, paramName)
}
assert.Fail(t, "Expected match groups not found", unmatchedParams)
}
}
}

207
src/hmnurl/urls.go Normal file
View File

@ -0,0 +1,207 @@
package hmnurl
import (
"git.handmade.network/hmn/hmn/src/oops"
"regexp"
"strconv"
"strings"
)
var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$")
func BuildHomepage() string {
return Url("/", nil)
}
var RegexLogin *regexp.Regexp = regexp.MustCompile("^/login$")
func BuildLogin() string {
return Url("/login", nil)
}
var RegexLogout *regexp.Regexp = regexp.MustCompile("^/logout$")
func BuildLogout() string {
return Url("/logout", nil)
}
var RegexManifesto *regexp.Regexp = regexp.MustCompile("^/manifesto$")
func BuildManifesto() string {
return Url("/manifesto", nil)
}
var RegexAbout *regexp.Regexp = regexp.MustCompile("^/about$")
func BuildAbout() string {
return Url("/about", nil)
}
var RegexCodeOfConduct *regexp.Regexp = regexp.MustCompile("^/code-of-conduct$")
func BuildCodeOfConduct() string {
return Url("/code-of-conduct", nil)
}
var RegexCommunicationGuidelines *regexp.Regexp = regexp.MustCompile("^/communication-guidelines$")
func BuildCommunicationGuidelines() string {
return Url("/communication-guidelines", nil)
}
var RegexContactPage *regexp.Regexp = regexp.MustCompile("^/contact$")
func BuildContactPage() string {
return Url("/contact", nil)
}
var RegexMonthlyUpdatePolicy *regexp.Regexp = regexp.MustCompile("^/monthly-update-policy$")
func BuildMonthlyUpdatePolicy() string {
return Url("/monthly-update-policy", nil)
}
var RegexProjectSubmissionGuidelines *regexp.Regexp = regexp.MustCompile("^/project-guidelines$")
func BuildProjectSubmissionGuidelines() string {
return Url("/project-guidelines", nil)
}
var RegexFeed *regexp.Regexp = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
func BuildFeed() string {
return Url("/feed", nil)
}
func BuildFeedWithPage(page int) string {
if page < 1 {
panic(oops.New(nil, "Invalid feed page (%d), must be >= 1", page))
}
if page == 1 {
return BuildFeed()
}
return Url("/feed/"+strconv.Itoa(page), nil)
}
var RegexForumThread *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`)
func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
}
var builder strings.Builder
builder.WriteString("/forums")
for _, subforum := range subforums {
subforum = strings.TrimSpace(subforum)
if strings.Contains(subforum, "/") {
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
}
if len(subforum) == 0 {
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
}
builder.WriteRune('/')
builder.WriteString(subforum)
}
builder.WriteString("/t/")
builder.WriteString(strconv.Itoa(threadId))
if page > 1 {
builder.WriteRune('/')
builder.WriteString(strconv.Itoa(page))
}
return ProjectUrl(builder.String(), nil, projectSlug)
}
var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`)
func BuildForumCategory(projectSlug string, subforums []string, page int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
}
var builder strings.Builder
builder.WriteString("/forums")
for _, subforum := range subforums {
subforum = strings.TrimSpace(subforum)
if strings.Contains(subforum, "/") {
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
}
if len(subforum) == 0 {
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
}
builder.WriteRune('/')
builder.WriteString(subforum)
}
if page > 1 {
builder.WriteRune('/')
builder.WriteString(strconv.Itoa(page))
}
return ProjectUrl(builder.String(), nil, projectSlug)
}
var RegexForumPost *regexp.Regexp = regexp.MustCompile(``) // TODO(asaf): Complete this and test it
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
var builder strings.Builder
builder.WriteString("/forums")
for _, subforum := range subforums {
subforum = strings.TrimSpace(subforum)
if strings.Contains(subforum, "/") {
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
}
if len(subforum) == 0 {
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
}
builder.WriteRune('/')
builder.WriteString(subforum)
}
builder.WriteString("/t/")
builder.WriteString(strconv.Itoa(threadId))
builder.WriteString("/p/")
builder.WriteString(strconv.Itoa(postId))
return ProjectUrl(builder.String(), nil, projectSlug)
}
var RegexProjectCSS *regexp.Regexp = regexp.MustCompile("^/assets/project.css$")
func BuildProjectCSS(color string) string {
return Url("/assets/project.css", []Q{Q{"color", color}})
}
var RegexPublic *regexp.Regexp = regexp.MustCompile("^/public/.+$")
func BuildPublic(filepath string) string {
filepath = strings.Trim(filepath, "/")
if len(strings.TrimSpace(filepath)) == 0 {
panic(oops.New(nil, "Attempted to build a /public url with no path"))
}
var builder strings.Builder
builder.WriteString("/public")
pathParts := strings.Split(filepath, "/")
for _, part := range pathParts {
part = strings.TrimSpace(part)
if len(part) == 0 {
panic(oops.New(nil, "Attempted to build a /public url with blank path segments: %s", filepath))
}
builder.WriteRune('/')
builder.WriteString(part)
}
return Url(builder.String(), nil)
}
var RegexCatchAll *regexp.Regexp = regexp.MustCompile("")

View File

@ -4,6 +4,7 @@ import (
"context"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4/pgxpool"
)
@ -33,6 +34,96 @@ type Category struct {
Depth int `db:"depth"` // TODO: What is this?
}
type CategoryLineageBuilder struct {
Tree map[int]*CategoryTreeNode
CategoryCache map[int][]*Category
SlugCache map[int][]string
}
func MakeCategoryLineageBuilder(fullCategoryTree map[int]*CategoryTreeNode) *CategoryLineageBuilder {
return &CategoryLineageBuilder{
Tree: fullCategoryTree,
CategoryCache: make(map[int][]*Category),
SlugCache: make(map[int][]string),
}
}
func (cl *CategoryLineageBuilder) GetLineage(catId int) []*Category {
_, ok := cl.CategoryCache[catId]
if !ok {
cl.CategoryCache[catId] = cl.Tree[catId].GetLineage()
}
return cl.CategoryCache[catId]
}
func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
_, ok := cl.SlugCache[catId]
if !ok {
lineage := cl.GetLineage(catId)
result := make([]string, 0, len(lineage))
for _, cat := range lineage {
name := ""
if cat.Slug != nil {
name = *cat.Slug
}
result = append(result, name)
}
cl.SlugCache[catId] = result
}
return cl.SlugCache[catId]
}
type CategoryTreeNode struct {
Category
Parent *CategoryTreeNode
}
func (node *CategoryTreeNode) GetLineage() []*Category {
current := node
length := 0
for current != nil {
current = current.Parent
length += 1
}
result := make([]*Category, length)
current = node
for i := length - 1; i >= 0; i -= 1 {
result[i] = &current.Category
current = current.Parent
}
return result
}
func GetFullCategoryTree(ctx context.Context, conn *pgxpool.Pool) map[int]*CategoryTreeNode {
type categoryRow struct {
Cat Category `db:"cat"`
}
rows, err := db.Query(ctx, conn, categoryRow{},
`
SELECT $columns
FROM
handmade_category as cat
`,
)
if err != nil {
panic(oops.New(err, "Failed to fetch category tree"))
}
rowsSlice := rows.ToSlice()
catTreeMap := make(map[int]*CategoryTreeNode, len(rowsSlice))
for _, row := range rowsSlice {
cat := row.(*categoryRow).Cat
catTreeMap[cat.ID] = &CategoryTreeNode{Category: cat}
}
for _, node := range catTreeMap {
if node.ParentID != nil {
node.Parent = catTreeMap[*node.ParentID]
}
}
return catTreeMap
}
/*
Gets the category and its parent categories, starting from the root and working toward the
category itself. Useful for breadcrumbs and the like.

View File

@ -9,6 +9,18 @@ const HMNProjectID = 1
var ProjectType = reflect.TypeOf(Project{})
type ProjectLifecycle int
const (
ProjectLifecycleUnapproved = iota
ProjectLifecycleApprovalRequired
ProjectLifecycleActive
ProjectLifecycleHiatus
ProjectLifecycleDead
ProjectLifecycleLTSRequired
ProjectLifecycleLTS
)
type Project struct {
ID int `db:"id"`
@ -17,6 +29,8 @@ type Project struct {
Blurb *string `db:"blurb"`
Description *string `db:"description"`
Lifecycle ProjectLifecycle `db:"lifecycle"`
Color1 string `db:"color_1"`
Color2 string `db:"color_2"`

19
src/utils/utils.go Normal file
View File

@ -0,0 +1,19 @@
package utils
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}
func IntMax(a, b int) int {
if a > b {
return a
}
return b
}
func IntClamp(min, t, max int) int {
return IntMax(min, IntMin(t, max))
}

View File

@ -1,7 +1,6 @@
package website
import (
"fmt"
"math"
"net/http"
"strconv"
@ -12,6 +11,7 @@ import (
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
)
type FeedData struct {
@ -32,7 +32,8 @@ func Feed(c *RequestContext) ResponseData {
handmade_post AS post
WHERE
post.category_kind = ANY ($1)
AND NOT deleted
AND deleted = FALSE
AND post.thread_id IS NOT NULL
`,
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
)
@ -49,11 +50,11 @@ func Feed(c *RequestContext) ResponseData {
if pageParsed, err := strconv.Atoi(pageString); err == nil {
page = pageParsed
} else {
return c.Redirect("/feed", http.StatusSeeOther)
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
}
}
if page < 1 || numPages < page {
return c.Redirect("/feed", http.StatusSeeOther)
return c.Redirect(hmnurl.BuildFeedWithPage(utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
}
howManyPostsToSkip := (page - 1) * postsPerPage
@ -62,10 +63,10 @@ func Feed(c *RequestContext) ResponseData {
Current: page,
Total: numPages,
FirstUrl: hmnurl.Url("/feed", nil),
LastUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", numPages), nil),
NextUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", page+1), nil),
PreviousUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", page-1), nil),
FirstUrl: hmnurl.BuildFeed(),
LastUrl: hmnurl.BuildFeedWithPage(numPages),
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
}
var currentUserId *int
@ -117,8 +118,28 @@ func Feed(c *RequestContext) ResponseData {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
}
categoryUrls := GetAllCategoryUrls(c.Context(), c.Conn)
c.Perf.StartBlock("SQL", "Fetch category tree")
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
c.Perf.EndBlock()
categoryUrlCache := make(map[int]string)
getCategoryUrl := func(subdomain string, cat *models.Category) string {
_, ok := categoryUrlCache[cat.ID]
if !ok {
lineageNames := lineageBuilder.GetLineageSlugs(cat.ID)
switch cat.Kind {
case models.CatKindForum:
categoryUrlCache[cat.ID] = hmnurl.BuildForumCategory(subdomain, lineageNames[1:], 1)
// TODO(asaf): Add more kinds!!!
default:
categoryUrlCache[cat.ID] = ""
}
}
return categoryUrlCache[cat.ID]
}
c.Perf.StartBlock("FEED", "Build post items")
var postItems []templates.PostListItem
for _, iPostResult := range posts.ToSlice() {
postResult := iPostResult.(*feedPostQuery)
@ -130,32 +151,35 @@ func Feed(c *RequestContext) ResponseData {
hasRead = true
}
parents := postResult.Cat.GetHierarchy(c.Context(), c.Conn)
var breadcrumbs []templates.Breadcrumb
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: *postResult.Proj.Name,
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Subdomain()),
})
for _, parent := range parents {
name := *parent.Name
if parent.ParentID == nil {
switch parent.Kind {
case models.CatKindForum:
name = "Forums"
case models.CatKindBlog:
name = "Blog"
if postResult.Post.CategoryKind == models.CatKindLibraryResource {
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it
} else {
lineage := lineageBuilder.GetLineage(postResult.Cat.ID)
for i, cat := range lineage {
name := *cat.Name
if i == 0 {
switch cat.Kind {
case models.CatKindForum:
name = "Forums"
case models.CatKindBlog:
name = "Blog"
}
}
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: name,
Url: getCategoryUrl(postResult.Proj.Subdomain(), cat),
})
}
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: name,
Url: categoryUrls[parent.ID],
})
}
postItems = append(postItems, templates.PostListItem{
Title: postResult.Thread.Title,
Url: PostUrl(postResult.Post, postResult.Post.CategoryKind, categoryUrls[postResult.Post.CategoryID]),
Url: hmnurl.BuildForumPost(postResult.Proj.Subdomain(), lineageBuilder.GetLineageSlugs(postResult.Cat.ID)[1:], postResult.Post.ID, postResult.Post.ThreadID),
User: templates.UserToTemplate(&postResult.User),
Date: postResult.Post.PostDate,
Breadcrumbs: breadcrumbs,
@ -164,6 +188,7 @@ func Feed(c *RequestContext) ResponseData {
Content: postResult.Post.Preview,
})
}
c.Perf.EndBlock()
baseData := getBaseData(c)
baseData.BodyClasses = append(baseData.BodyClasses, "feed")

View File

@ -46,29 +46,29 @@ func WrapStdHandler(h http.Handler) Handler {
type Middleware func(h Handler) Handler
func (rb *RouteBuilder) Handle(method string, regexStr string, h Handler) {
func (rb *RouteBuilder) Handle(method string, regex *regexp.Regexp, h Handler) {
h = rb.Middleware(h)
rb.Router.Routes = append(rb.Router.Routes, Route{
Method: method,
Regex: regexp.MustCompile(regexStr),
Regex: regex,
Handler: h,
})
}
func (rb *RouteBuilder) AnyMethod(regexStr string, h Handler) {
rb.Handle("", regexStr, h)
func (rb *RouteBuilder) AnyMethod(regex *regexp.Regexp, h Handler) {
rb.Handle("", regex, h)
}
func (rb *RouteBuilder) GET(regexStr string, h Handler) {
rb.Handle(http.MethodGet, regexStr, h)
func (rb *RouteBuilder) GET(regex *regexp.Regexp, h Handler) {
rb.Handle(http.MethodGet, regex, h)
}
func (rb *RouteBuilder) POST(regexStr string, h Handler) {
rb.Handle(http.MethodPost, regexStr, h)
func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) {
rb.Handle(http.MethodPost, regex, h)
}
func (rb *RouteBuilder) StdHandler(regexStr string, h http.Handler) {
rb.Handle("", regexStr, WrapStdHandler(h))
func (rb *RouteBuilder) StdHandler(regex *regexp.Regexp, h http.Handler) {
rb.Handle("", regex, WrapStdHandler(h))
}
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -31,9 +31,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
logPerf := TrackRequestPerf(c, perfCollector)
defer logPerf()
defer LogContextErrors(c, res)
res = h(c)
return h(c)
LogContextErrors(c, res)
return
}
},
}
@ -46,14 +47,15 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
logPerf := TrackRequestPerf(c, perfCollector)
defer logPerf()
defer LogContextErrors(c, res)
ok, errRes := LoadCommonWebsiteData(c)
if !ok {
return errRes
}
return h(c)
res = h(c)
LogContextErrors(c, res)
return
}
}
@ -65,29 +67,31 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
logPerf := TrackRequestPerf(c, perfCollector)
defer logPerf()
defer LogContextErrors(c, res)
ok, errRes := LoadCommonWebsiteData(c)
if !ok {
return errRes
}
if !c.CurrentProject.IsHMN() {
res := c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
return res
res = c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
return
}
return h(c)
res = h(c)
LogContextErrors(c, res)
return
}
}
routes.POST("^/login$", Login)
routes.GET("^/logout$", Logout)
routes.StdHandler("^/public/.*$",
// TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware.
routes.POST(hmnurl.RegexLogin, Login)
routes.GET(hmnurl.RegexLogout, Logout)
routes.StdHandler(hmnurl.RegexPublic,
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
)
mainRoutes.GET("^/$", func(c *RequestContext) ResponseData {
mainRoutes.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
if c.CurrentProject.IsHMN() {
return Index(c)
} else {
@ -95,24 +99,24 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
panic("route not implemented")
}
})
staticPages.GET("^/manifesto$", Manifesto)
staticPages.GET("^/about$", About)
staticPages.GET("^/code-of-conduct$", CodeOfConduct)
staticPages.GET("^/communication-guidelines$", CommunicationGuidelines)
staticPages.GET("^/contact$", ContactPage)
staticPages.GET("^/monthly-update-policy$", MonthlyUpdatePolicy)
staticPages.GET("^/project-guidelines$", ProjectSubmissionGuidelines)
staticPages.GET(hmnurl.RegexManifesto, Manifesto)
staticPages.GET(hmnurl.RegexAbout, About)
staticPages.GET(hmnurl.RegexCodeOfConduct, CodeOfConduct)
staticPages.GET(hmnurl.RegexCommunicationGuidelines, CommunicationGuidelines)
staticPages.GET(hmnurl.RegexContactPage, ContactPage)
staticPages.GET(hmnurl.RegexMonthlyUpdatePolicy, MonthlyUpdatePolicy)
staticPages.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
mainRoutes.GET(`^/feed(/(?P<page>.+)?)?$`, Feed)
mainRoutes.GET(hmnurl.RegexFeed, Feed)
// TODO(asaf): Trailing slashes break these
mainRoutes.GET(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`, ForumThread)
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost)
mainRoutes.GET(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`, ForumCategory)
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
mainRoutes.GET("^/assets/project.css$", ProjectCSS)
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
mainRoutes.AnyMethod("", FourOhFour)
mainRoutes.AnyMethod(hmnurl.RegexCatchAll, FourOhFour)
return router
}

View File

@ -23,7 +23,9 @@ func GetAllCategoryUrls(ctx context.Context, conn *pgxpool.Pool) map[int]string
FROM
handmade_category AS cat
JOIN handmade_project AS project ON project.id = cat.project_id
`,
WHERE
cat.kind != 6
`, // TODO(asaf): Clean up the db and remove the cat.kind != 6 check
)
if err != nil {
panic(err)
@ -42,7 +44,8 @@ func GetProjectCategoryUrls(ctx context.Context, conn *pgxpool.Pool, projectId .
JOIN handmade_project AS project ON project.id = cat.project_id
WHERE
project.id = ANY ($1)
`,
AND cat.kind != 6
`, // TODO(asaf): Clean up the db and remove the cat.kind != 6 check
projectId,
)
if err != nil {
@ -91,24 +94,16 @@ func makeCategoryUrls(rows []interface{}) map[int]string {
}
func CategoryUrl(subdomain string, cats ...*models.Category) string {
path := ""
for i, cat := range cats {
if i == 0 {
switch cat.Kind {
case models.CatKindBlog:
path += "/blogs"
case models.CatKindForum:
path += "/forums"
// TODO: All cat types?
default:
return ""
}
} else {
path += "/" + *cat.Slug
}
catNames := make([]string, 0, len(cats))
for _, cat := range cats {
catNames = append(catNames, *cat.Name)
}
switch cats[0].Kind {
case models.CatKindForum:
return hmnurl.BuildForumCategory(subdomain, catNames[1:], 1)
default:
return ""
}
return hmnurl.ProjectUrl(path, nil, subdomain)
}
func PostUrl(post models.Post, catKind models.CategoryKind, categoryUrl string) string {