Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c0ced322d | |||
| 413bbd0c7f | |||
| 1ccb4b7adc | |||
| da622a4927 | |||
| 4488ccc689 | |||
| 98329f3b05 | |||
| ecef4f11dc | |||
| 9df5853942 | |||
| c17e0c94c8 | |||
| b70dda997a | |||
| d10e6aad70 | |||
| e1d58ab635 | |||
| 3b6f981b81 | |||
| d93e31e92a | |||
| 43a91313d8 | |||
| 682811f769 | |||
| cece87f9dc | |||
| e884bb61cb | |||
| 68085ab742 | |||
| 7c210a4855 |
+1
-1
@@ -43,7 +43,7 @@ language_options:
|
||||
progress: 96
|
||||
- label: "Русский"
|
||||
value: "ru_RU"
|
||||
progress: 80
|
||||
progress: 100
|
||||
- label: "简体中文"
|
||||
value: "zh_CN"
|
||||
progress: 100
|
||||
|
||||
+400
-397
File diff suppressed because it is too large
Load Diff
@@ -110,6 +110,7 @@ var migrations = []Migration{
|
||||
NewMigration("v2.0.1", "change avatar type to text", updateAvatarType, false),
|
||||
NewMigration("v2.0.2", "add reasoning content to ai conversation record", addAIConversationReasoningContent, false),
|
||||
NewMigration("v2.0.3", "add require email verification login setting", addRequireEmailVerification, true),
|
||||
NewMigration("v2.0.4", "repair missing advanced site settings", repairAdvancedSiteInfo, true),
|
||||
}
|
||||
|
||||
func GetMigrations() []Migration {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func repairAdvancedSiteInfo(ctx context.Context, x *xorm.Engine) error {
|
||||
advanced := &entity.SiteInfo{}
|
||||
exists, err := x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeAdvanced}).Get(advanced)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
write := &entity.SiteInfo{}
|
||||
exists, err = x.Context(ctx).Where(builder.Eq{"type": constant.SiteTypeWrite}).Get(write)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
siteWrite := &schema.SiteWriteResp{}
|
||||
if err := json.Unmarshal([]byte(write.Content), siteWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := json.Marshal(&schema.SiteAdvancedResp{
|
||||
MaxImageSize: siteWrite.MaxImageSize,
|
||||
MaxAttachmentSize: siteWrite.MaxAttachmentSize,
|
||||
MaxImageMegapixel: siteWrite.MaxImageMegapixel,
|
||||
AuthorizedImageExtensions: siteWrite.AuthorizedImageExtensions,
|
||||
AuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = x.Context(ctx).Insert(&entity.SiteInfo{
|
||||
Type: constant.SiteTypeAdvanced,
|
||||
Content: string(content),
|
||||
Status: 1,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func TestRepairAdvancedSiteInfoAddsMissingSettings(t *testing.T) {
|
||||
x, err := xorm.NewEngine("sqlite", ":memory:")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = x.Close()
|
||||
}()
|
||||
require.NoError(t, x.Sync(new(entity.SiteInfo)))
|
||||
|
||||
_, err = x.Insert(&entity.SiteInfo{
|
||||
Type: constant.SiteTypeWrite,
|
||||
Content: `{"max_image_size":5}`,
|
||||
Status: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var repairMigration Migration
|
||||
for _, m := range GetMigrations() {
|
||||
if m.Version() == "v2.0.4" {
|
||||
repairMigration = m
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, repairMigration)
|
||||
require.NoError(t, repairMigration.Migrate(context.Background(), x))
|
||||
|
||||
advanced := &entity.SiteInfo{}
|
||||
exists, err := x.Where("type = ?", constant.SiteTypeAdvanced).Get(advanced)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
assert.JSONEq(t, `{
|
||||
"max_image_size": 5,
|
||||
"max_attachment_size": 0,
|
||||
"max_image_megapixel": 0,
|
||||
"authorized_image_extensions": null,
|
||||
"authorized_attachment_extensions": null
|
||||
}`, advanced.Content)
|
||||
}
|
||||
|
||||
func TestRepairAdvancedSiteInfoPreservesExistingSettings(t *testing.T) {
|
||||
x, err := xorm.NewEngine("sqlite", ":memory:")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = x.Close()
|
||||
}()
|
||||
require.NoError(t, x.Sync(new(entity.SiteInfo)))
|
||||
|
||||
const existingContent = `{"max_image_size":99}`
|
||||
_, err = x.Insert(
|
||||
&entity.SiteInfo{
|
||||
Type: constant.SiteTypeWrite,
|
||||
Content: `{invalid`,
|
||||
Status: 1,
|
||||
},
|
||||
&entity.SiteInfo{
|
||||
Type: constant.SiteTypeAdvanced,
|
||||
Content: existingContent,
|
||||
Status: 1,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, repairAdvancedSiteInfo(context.Background(), x))
|
||||
|
||||
advanced := &entity.SiteInfo{}
|
||||
exists, err := x.Where("type = ?", constant.SiteTypeAdvanced).Get(advanced)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
assert.JSONEq(t, existingContent, advanced.Content)
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package tag_common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -171,10 +170,20 @@ func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag
|
||||
session := tr.data.DB.Context(ctx)
|
||||
|
||||
if len(tag.SlugName) > 0 {
|
||||
// Both sides lowered, so the search is case-insensitive.
|
||||
//
|
||||
// This previously read LOWER(%s) formatted against the *search term*,
|
||||
// which put the function name into the value: the query became
|
||||
// slug_name LIKE '%LOWER(coco)%' and could never match. Only the
|
||||
// display_name clause did anything, and that is case-sensitive on
|
||||
// Postgres, so typing a tag in lower case -- which is how tags are
|
||||
// written and therefore how anyone types them -- returned nothing at all
|
||||
// and read as "no such tag".
|
||||
search := searchTermForTag(tag.SlugName)
|
||||
mainTagCond := builder.And(
|
||||
builder.Or(
|
||||
builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)},
|
||||
builder.Like{"display_name", tag.SlugName},
|
||||
builder.Like{"LOWER(slug_name)", search},
|
||||
builder.Like{"LOWER(display_name)", search},
|
||||
),
|
||||
builder.Eq{"main_tag_id": 0},
|
||||
)
|
||||
@@ -293,3 +302,10 @@ func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// searchTermForTag normalises a tag search term. Lowering it here, and lowering
|
||||
// the columns in the query, is what makes the search case-insensitive: tags are
|
||||
// written in lower case, so that is how people type them.
|
||||
func searchTermForTag(term string) string {
|
||||
return strings.ToLower(strings.TrimSpace(term))
|
||||
}
|
||||
|
||||
@@ -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 tag_common
|
||||
|
||||
import "testing"
|
||||
|
||||
// The bug: the search term was formatted into LOWER(%s), which put the function
|
||||
// name into the value rather than applying it to the column, so the query became
|
||||
// slug_name LIKE '%LOWER(coco)%' and matched nothing. Only display_name did any
|
||||
// work, and that is case-sensitive on Postgres -- so typing a tag the way tags
|
||||
// are actually written returned "no such tag".
|
||||
func TestSearchTermIsLoweredNotWrapped(t *testing.T) {
|
||||
for _, in := range []string{"Coco", "COCO", "coco"} {
|
||||
got := searchTermForTag(in)
|
||||
if got != "coco" {
|
||||
t.Errorf("searchTermForTag(%q) = %q, want %q", in, got, "coco")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func GetTagPermission(ctx context.Context, status int, canEdit, canDelete, canMe
|
||||
})
|
||||
}
|
||||
|
||||
if canRecover && status == entity.QuestionStatusDeleted {
|
||||
if canRecover && status == entity.TagStatusDeleted {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
Action: "undelete",
|
||||
Name: translator.Tr(lang, undeleteActionName),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderLinkIsUrl(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"absolute http URL", "http://example.com/path?q=1#f", true},
|
||||
{"absolute https URL", "https://example.com", true},
|
||||
{"ftp URL", "ftp://example.com/file", true},
|
||||
{"uppercase scheme and host", "HTTP://EXAMPLE.COM", false},
|
||||
{"bare domain", "example.com", true},
|
||||
{"bare domain with path", "example.com/questions/123", true},
|
||||
{"www subdomain", "www.example.com", true},
|
||||
{"bare IP", "10.0.0.1", true},
|
||||
{"IP with port and path", "10.0.0.1:8080/a", true},
|
||||
{"host with port", "localhost:8080", true},
|
||||
{"domain with port and path", "example.com:8080/x", true},
|
||||
{"IPv6 with port", "[::1]:8080", true},
|
||||
{"userinfo", "user:pass@example.com", true},
|
||||
{"mailto", "mailto:a@b.com", true},
|
||||
{"email-like destination", "a@b.co", true},
|
||||
{"userinfo without scheme", "user@h.co", true},
|
||||
{"trailing dot FQDN", "example.com.", true},
|
||||
{"empty", "", false},
|
||||
{"single word", "foo", false},
|
||||
{"path segment no dot", "questions/123", false},
|
||||
{"absolute path", "/questions/123", true},
|
||||
{"scheme-less authority path", "//cdn.example.com/x", true},
|
||||
{"anchor", "#section", false},
|
||||
{"leading dot", ".hidden", false},
|
||||
{"javascript scheme", "javascript:alert(1)", false},
|
||||
{"tel scheme", "tel:+1234", false},
|
||||
{"host with leading dot", "http://.example.com", false},
|
||||
{"trailing colon", "example.com:", false},
|
||||
{"single label with scheme", "http://localhost", true},
|
||||
{"single label no scheme no port", "localhost", false},
|
||||
{"not a url", "not a url", false},
|
||||
{"whitespace in path", "h.co/p q", false},
|
||||
{"label with leading hyphen", "-ex.com", false},
|
||||
{"label with trailing hyphen", "ex-.com", false},
|
||||
{"invalid IPv4 quad", "999.1.1.1", false},
|
||||
{"IPv4 with leading zeros", "01.2.3.4", false},
|
||||
{"three letter domain", "a.b", false},
|
||||
}
|
||||
r := &DangerousHTMLRenderer{}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, r.renderLinkIsUrl(tc.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user