chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+31
View File
@@ -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
}
+36
View File
@@ -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
}
+152
View File
@@ -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
}
+67
View File
@@ -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
}
+57
View File
@@ -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)
}
+131
View File
@@ -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,
})
}
}
+184
View File
@@ -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)
})
}
+58
View File
@@ -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]
}
+43
View File
@@ -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"
}
+30
View File
@@ -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)
}
+36
View File
@@ -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
}