chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import "unicode"
|
||||
|
||||
func IsChinese(str string) bool {
|
||||
for _, v := range str {
|
||||
if unicode.Is(unicode.Han, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import "strings"
|
||||
|
||||
func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool {
|
||||
if len(allowEmailDomains) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, domain := range allowEmailDomains {
|
||||
if strings.HasSuffix(email, domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// IsUnAuthorizedExtension check whether the file extension is not in the allowedExtensions
|
||||
// WANING Only checks the file extension is not reliable, but `http.DetectContentType` and `mimetype` are not reliable for all file types.
|
||||
func IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool {
|
||||
ext := strings.ToLower(strings.Trim(filepath.Ext(fileName), "."))
|
||||
return !slices.Contains(allowedExtensions, ext)
|
||||
}
|
||||
|
||||
// DecodeAndCheckImageFile currently answers support image type is
|
||||
// `image/jpeg, image/jpg, image/png, image/gif, image/webp`
|
||||
func DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool {
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), "."))
|
||||
switch ext {
|
||||
case "jpg", "jpeg", "png", "gif":
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, formatSpecificConfigCheck) {
|
||||
return false
|
||||
}
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, formatSpecificImageCheck) {
|
||||
return false
|
||||
}
|
||||
case "webp":
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, webpImageConfigCheck) {
|
||||
return false
|
||||
}
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, webpImageCheck) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, ext string,
|
||||
checker func(file io.Reader, ext string, maxImageMegapixel int) error) bool {
|
||||
file, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
log.Errorf("open file error: %v", err)
|
||||
return false
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if err = checker(file, ext, maxImageMegapixel); err != nil {
|
||||
log.Errorf("check image format error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// formatSpecificConfigCheck decodes image config using a format-specific decoder
|
||||
// based on the file extension. This avoids calling image.DecodeConfig() which
|
||||
// dispatches by magic bytes and can invoke unintended decoders (e.g., TIFF)
|
||||
// registered by transitive dependencies.
|
||||
func formatSpecificConfigCheck(file io.Reader, ext string, maxImageMegapixel int) error {
|
||||
var config image.Config
|
||||
var err error
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
config, err = jpeg.DecodeConfig(file)
|
||||
case "png":
|
||||
config, err = png.DecodeConfig(file)
|
||||
case "gif":
|
||||
config, err = gif.DecodeConfig(file)
|
||||
default:
|
||||
return fmt.Errorf("unsupported image format: %s", ext)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode image config error: %v", err)
|
||||
}
|
||||
if imageSizeTooLarge(config, maxImageMegapixel) {
|
||||
return fmt.Errorf("image size too large")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatSpecificImageCheck fully decodes the image using a format-specific decoder.
|
||||
func formatSpecificImageCheck(file io.Reader, ext string, _ int) error {
|
||||
var err error
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
_, err = jpeg.Decode(file)
|
||||
case "png":
|
||||
_, err = png.Decode(file)
|
||||
case "gif":
|
||||
_, err = gif.Decode(file)
|
||||
default:
|
||||
return fmt.Errorf("unsupported image format: %s", ext)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode image error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webpImageConfigCheck(file io.Reader, _ string, maxImageMegapixel int) error {
|
||||
config, err := webp.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode webp image config error: %v", err)
|
||||
}
|
||||
if imageSizeTooLarge(config, maxImageMegapixel) {
|
||||
return fmt.Errorf("image size too large")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webpImageCheck(file io.Reader, _ string, _ int) error {
|
||||
_, err := webp.Decode(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode webp image error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageSizeTooLarge(config image.Config, maxImageMegapixel int) bool {
|
||||
return config.Width*config.Height > maxImageMegapixel
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
levelD = iota
|
||||
LevelC
|
||||
LevelB
|
||||
LevelA
|
||||
LevelS
|
||||
)
|
||||
|
||||
const (
|
||||
PasswordCannotContainSpaces = "error.password.space_invalid"
|
||||
)
|
||||
|
||||
// CheckPassword checks the password strength
|
||||
func CheckPassword(password string) error {
|
||||
if strings.Contains(password, " ") {
|
||||
return errors.New(PasswordCannotContainSpaces)
|
||||
}
|
||||
|
||||
// TODO Currently there is no requirement for password strength
|
||||
minLevel := 0
|
||||
|
||||
// The password strength level is initialized to D.
|
||||
// The regular is used to verify the password strength.
|
||||
// If the matching is successful, the password strength increases by 1
|
||||
level := levelD
|
||||
patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`}
|
||||
for _, pattern := range patternList {
|
||||
match, _ := regexp.MatchString(pattern, password)
|
||||
if match {
|
||||
level++
|
||||
}
|
||||
}
|
||||
|
||||
// If the final password strength falls below the required minimum strength, return with an error
|
||||
if level < minLevel {
|
||||
return fmt.Errorf("the password does not satisfy the current policy requirements")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/apache/answer/configs"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type PathIgnore struct {
|
||||
Users []string `yaml:"users"`
|
||||
Questions []string `yaml:"questions"`
|
||||
}
|
||||
|
||||
var (
|
||||
ignorePathInit sync.Once
|
||||
pathIgnore = &PathIgnore{}
|
||||
)
|
||||
|
||||
func initPathIgnore() {
|
||||
if err := yaml.Unmarshal(configs.PathIgnore, pathIgnore); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsUsersIgnorePath checks whether the username is in ignore path
|
||||
func IsUsersIgnorePath(username string) bool {
|
||||
ignorePathInit.Do(initPathIgnore)
|
||||
return slices.Contains(pathIgnore.Users, username)
|
||||
}
|
||||
|
||||
// IsQuestionsIgnorePath checks whether the questionID is in ignore path
|
||||
func IsQuestionsIgnorePath(questionID string) bool {
|
||||
ignorePathInit.Do(initPathIgnore)
|
||||
return slices.Contains(pathIgnore.Questions, questionID)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/pkg/obj"
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
)
|
||||
|
||||
const (
|
||||
QuestionLinkTypeURL = 1
|
||||
QuestionLinkTypeID = 2
|
||||
)
|
||||
|
||||
type QuestionLink struct {
|
||||
LinkType int
|
||||
QuestionID string
|
||||
AnswerID string
|
||||
}
|
||||
|
||||
func GetQuestionLink(content string) []QuestionLink {
|
||||
uniqueIDs := make(map[string]struct{})
|
||||
var questionLinks []QuestionLink
|
||||
|
||||
// use two pointer to find the link
|
||||
left, right := 0, 0
|
||||
for right < len(content) {
|
||||
// find "/questions/" or "#"
|
||||
switch {
|
||||
case right+11 < len(content) && content[right:right+11] == "/questions/":
|
||||
left = right
|
||||
right += 11
|
||||
processURL(content, &left, &right, uniqueIDs, &questionLinks)
|
||||
case content[right] == '#':
|
||||
left = right + 1
|
||||
right = left
|
||||
processID(content, &left, &right, uniqueIDs, &questionLinks)
|
||||
default:
|
||||
right++
|
||||
}
|
||||
}
|
||||
|
||||
return questionLinks
|
||||
}
|
||||
|
||||
func processURL(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {
|
||||
for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {
|
||||
*right++
|
||||
}
|
||||
questionID := content[*left+len("/questions/") : *right]
|
||||
|
||||
answerID := ""
|
||||
if *right < len(content) && content[*right] == '/' {
|
||||
*left = *right + 1
|
||||
*right = *left
|
||||
for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {
|
||||
*right++
|
||||
}
|
||||
answerID = content[*left:*right]
|
||||
}
|
||||
|
||||
addUniqueID(questionID, answerID, QuestionLinkTypeURL, uniqueIDs, questionLinks)
|
||||
}
|
||||
|
||||
func processID(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {
|
||||
for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) {
|
||||
*right++
|
||||
}
|
||||
id := content[*left:*right]
|
||||
addUniqueID(id, "", QuestionLinkTypeID, uniqueIDs, questionLinks)
|
||||
}
|
||||
|
||||
func isDigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
func isLetter(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
func addUniqueID(questionID, answerID string, linkType int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) {
|
||||
isAdd := false
|
||||
if answerID != "" {
|
||||
objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(answerID))
|
||||
if err != nil {
|
||||
answerID = ""
|
||||
} else if objectType == constant.AnswerObjectType {
|
||||
if _, ok := uniqueIDs[answerID]; !ok {
|
||||
uniqueIDs[answerID] = struct{}{}
|
||||
isAdd = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(questionID)); err == nil {
|
||||
if _, ok := uniqueIDs[questionID]; !ok {
|
||||
uniqueIDs[questionID] = struct{}{}
|
||||
isAdd = true
|
||||
if objectType == constant.AnswerObjectType {
|
||||
answerID = questionID
|
||||
questionID = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isAdd {
|
||||
*questionLinks = append(*questionLinks, QuestionLink{
|
||||
LinkType: linkType,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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 checker_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetQuestionLink(t *testing.T) {
|
||||
// Step 1: Test empty content
|
||||
t.Run("Empty content", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
|
||||
// Step 2: Test content without link or ID
|
||||
t.Run("Content without link or ID", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("This is a random text")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
|
||||
// Step 3: Test content with valid question link
|
||||
t.Run("Valid question link", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("Check this question: https://example.com/questions/10010000000000060")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 4: Test content with valid question and answer link
|
||||
t.Run("Valid question and answer link", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("Check this answer: https://example.com/questions/10010000000000060/10020000000000060?from=copy")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "10020000000000060",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 5: Test content with #questionID
|
||||
t.Run("Content with #questionID", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("This is question #10010000000000060")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeID,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 6: Test content with #answerID
|
||||
t.Run("Content with #answerID", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("This is answer #10020000000000060")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeID,
|
||||
QuestionID: "",
|
||||
AnswerID: "10020000000000060",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 7: Test invalid question ID
|
||||
t.Run("Invalid question ID", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("https://example.com/questions/invalid")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
|
||||
// Step 8: Test invalid answer ID
|
||||
t.Run("Invalid answer ID", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("https://example.com/questions/10010000000000060/invalid")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 9: Test content with multiple links and IDs
|
||||
t.Run("Multiple links and IDs", func(t *testing.T) {
|
||||
content := "Question #10010000000000060 and https://example.com/questions/10010000000000060/10020000000000061 and https://example.com/questions/10010000000000065/10020000000000066 and another #10020000000000066"
|
||||
links := checker.GetQuestionLink(content)
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeID,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "10020000000000061",
|
||||
},
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000065",
|
||||
AnswerID: "10020000000000066",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 11: Test URL with www prefix
|
||||
t.Run("URL with www prefix", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("Check this question: https://www.example.com/questions/10010000000000060")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 12: Test URL without protocol
|
||||
t.Run("URL without protocol", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("Check this question: example.com/questions/10010000000000060")
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000060",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
|
||||
// Step 14: Test error id
|
||||
t.Run("Error id", func(t *testing.T) {
|
||||
links := checker.GetQuestionLink("https://example.com/questions/10110000000000060")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
|
||||
// step 15: SEO options
|
||||
t.Run("SEO options", func(t *testing.T) {
|
||||
content := `
|
||||
URL1: http://localhost:3000/questions/D1I2
|
||||
URL2: http://localhost:3000/questions/D1I2/hello
|
||||
URL3: http://localhost:3000/questions/10010000000000068
|
||||
URL4: http://localhost:3000/questions/10010000000000068/hello
|
||||
ERROR URL: http://localhost:3000/questions/AAAA/BBBB
|
||||
`
|
||||
links := checker.GetQuestionLink(content)
|
||||
assert.Equal(t, []checker.QuestionLink{
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "D1I2",
|
||||
AnswerID: "",
|
||||
},
|
||||
{
|
||||
LinkType: checker.QuestionLinkTypeURL,
|
||||
QuestionID: "10010000000000068",
|
||||
AnswerID: "",
|
||||
},
|
||||
}, links)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/apache/answer/configs"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/pkg/dir"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedUsernameMapping = make(map[string]bool)
|
||||
reservedUsernameInit sync.Once
|
||||
)
|
||||
|
||||
func initReservedUsername() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(path.ConfigFileDir, path.DefaultReservedUsernamesConfigFileName)
|
||||
if dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
// if reserved username file exists, read it and replace configuration
|
||||
reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath)
|
||||
if err == nil {
|
||||
configs.ReservedUsernames = reservedUsernamesJsonFile
|
||||
}
|
||||
}
|
||||
var usernames []string
|
||||
_ = json.Unmarshal(configs.ReservedUsernames, &usernames)
|
||||
for _, username := range usernames {
|
||||
reservedUsernameMapping[username] = true
|
||||
}
|
||||
}
|
||||
|
||||
// IsReservedUsername checks whether the username is reserved
|
||||
func IsReservedUsername(username string) bool {
|
||||
reservedUsernameInit.Do(initReservedUsername)
|
||||
return reservedUsernameMapping[username]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsURL(str string) bool {
|
||||
s := strings.ToLower(str)
|
||||
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
u, err := url.Parse(s)
|
||||
if err != nil || u.Scheme == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Host == "" && u.Fragment == "" && u.Opaque == "" {
|
||||
return false
|
||||
}
|
||||
return u.Scheme == "http" || u.Scheme == "https"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
usernameReg = regexp.MustCompile(`^[\w.\- ]{2,30}$`)
|
||||
)
|
||||
|
||||
func IsInvalidUsername(username string) bool {
|
||||
return !usernameReg.MatchString(username)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 checker
|
||||
|
||||
// IsNotZeroString check s is not empty string and is not "0"
|
||||
func IsNotZeroString(s string) bool {
|
||||
return len(s) > 0 && s != "0"
|
||||
}
|
||||
|
||||
// FilterEmptyString filter empty string from string slice
|
||||
func FilterEmptyString(strs []string) []string {
|
||||
var result []string
|
||||
for _, str := range strs {
|
||||
if IsNotZeroString(str) {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user