From 94032f4f9033ee052b68d1a9f87ce97210b4fda4 Mon Sep 17 00:00:00 2001 From: Matt Fellenz Date: Mon, 25 Mar 2024 10:05:24 -0700 Subject: [PATCH] Implement external exchanges (#99) * Implement external exchanges * test: translateUrl test cases * fix: double slash bug in translateUrl --------- Co-authored-by: httpjamesm --- src/routes/home.go | 58 +++++++++++++++++++++++++++-------------- src/routes/home_test.go | 32 +++++++++++++++++++++++ src/routes/question.go | 4 ++- 3 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 src/routes/home_test.go diff --git a/src/routes/home.go b/src/routes/home.go index 306d41c..5c546cd 100644 --- a/src/routes/home.go +++ b/src/routes/home.go @@ -20,7 +20,41 @@ type urlConversionRequest struct { URL string `form:"url" binding:"required"` } -var stackExchangeRegex = regexp.MustCompile(`https://(.+).stackexchange.com/questions/`) +var coreRegex = regexp.MustCompile(`(?:https?://)?(?:www\.)?([^/]+)(/(?:questions|q|a)/.+)`) + +// Will return `nil` if `rawUrl` is invalid. +func translateUrl(rawUrl string) string { + coreMatches := coreRegex.FindStringSubmatch(rawUrl) + if coreMatches == nil { + return "" + } + + domain := coreMatches[1] + rest := coreMatches[2] + + exchange := "" + if domain == "stackoverflow.com" { + // No exchange parameter needed. + } else if sub, found := strings.CutSuffix(domain, ".stackexchange.com"); found { + if sub == "" { + return "" + } else if strings.Contains(sub, ".") { + // Anything containing dots is interpreted as a full domain, so we use the correct full domain. + exchange = domain + } else { + exchange = sub + } + } else { + exchange = domain + } + + // Ensure we properly format the return string to avoid double slashes + if exchange == "" { + return rest + } else { + return fmt.Sprintf("/exchange/%s%s", exchange, rest) + } +} func PostHome(c *gin.Context) { body := urlConversionRequest{} @@ -33,16 +67,9 @@ func PostHome(c *gin.Context) { return } - soLink := body.URL + translated := translateUrl(body.URL) - // remove the www. - soLink = strings.ReplaceAll(soLink, "www.", "") - - // validate URL - isStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/questions/") - isShortenedStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/a/") || strings.HasPrefix(soLink, "https://stackoverflow.com/q/") - isStackExchange := stackExchangeRegex.MatchString(soLink) - if !isStackExchange && !isStackOverflow && !isShortenedStackOverflow { + if translated == "" { c.HTML(400, "home.html", gin.H{ "errorMessage": "Invalid stack overflow/exchange URL", "theme": c.MustGet("theme").(string), @@ -50,14 +77,5 @@ func PostHome(c *gin.Context) { return } - // if stack overflow, trim https://stackoverflow.com - if isStackOverflow || isShortenedStackOverflow { - c.Redirect(302, strings.TrimPrefix(soLink, "https://stackoverflow.com")) - return - } - - // if stack exchange, extract the subdomain - sub := stackExchangeRegex.FindStringSubmatch(soLink)[1] - - c.Redirect(302, fmt.Sprintf("/exchange/%s/%s", sub, strings.TrimPrefix(soLink, fmt.Sprintf("https://%s.stackexchange.com", sub)))) + c.Redirect(302, translated) } diff --git a/src/routes/home_test.go b/src/routes/home_test.go new file mode 100644 index 0000000..eda6d64 --- /dev/null +++ b/src/routes/home_test.go @@ -0,0 +1,32 @@ +package routes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTranslateUrl(t *testing.T) { + assert := assert.New(t) + + // Test with a Valid StackOverflow URL + assert.Equal("/questions/example-question", translateUrl("https://stackoverflow.com/questions/example-question"), "StackOverflow URL should not be modified") + + // Test with Complex Subdomain + assert.Equal("/exchange/meta.math.stackexchange.com/q/example-question", translateUrl("https://meta.math.stackexchange.com/q/example-question"), "Complex StackExchange subdomain should be used as full exchange") + + // Test with Non-StackExchange Domain + assert.Equal("/exchange/example.com/questions/example-question", translateUrl("https://example.com/questions/example-question"), "Non-StackExchange domain should be detected as exchange") + + // Test with Invalid URL + assert.Equal("", translateUrl("This is not a URL"), "Invalid URL should return an empty string") + + // Test with Empty String + assert.Equal("", translateUrl(""), "Empty string should return an empty string") + + // Test with Missing Path + assert.Equal("", translateUrl("https://stackoverflow.com"), "URL without path should return an empty string") + + // Test with Valid URL but Root Domain for StackExchange + assert.Equal("", translateUrl("https://stackexchange.com"), "Root StackExchange domain without subdomain should return an empty string") +} diff --git a/src/routes/question.go b/src/routes/question.go index 5a70c7f..f4f99f3 100644 --- a/src/routes/question.go +++ b/src/routes/question.go @@ -47,7 +47,9 @@ func ViewQuestion(c *gin.Context) { domain := "stackoverflow.com" - if params.Sub != "" { + if strings.Contains(params.Sub, ".") { + domain = params.Sub + } else if params.Sub != "" { domain = fmt.Sprintf("%s.stackexchange.com", params.Sub) }