Compare commits
15 commits
be35644fa0
...
d803ad1663
Author | SHA1 | Date | |
---|---|---|---|
d803ad1663 | |||
ebf42af71c | |||
|
99a93025d2 | ||
|
0d86ea0c43 | ||
|
c041114a20 | ||
|
f54189092f | ||
|
64a418dfc7 | ||
|
3ea5384241 | ||
|
745b45406d | ||
|
8f6d442a04 | ||
|
2e9fa11bb3 | ||
|
957a64d91a | ||
|
56a17f3565 | ||
|
4b9a473e12 | ||
|
5aad8a6918 |
29 changed files with 230 additions and 119 deletions
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
pipeline:
|
||||||
|
nulo-container:
|
||||||
|
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.nulo.in/nulo/forgejo
|
||||||
|
tag: v1.20.4-0
|
||||||
|
registry: https://gitea.nulo.in
|
||||||
|
username: Nulo
|
||||||
|
password:
|
||||||
|
from_secret: registry_secret
|
||||||
|
secrets: [REGISTRY_SECRET]
|
||||||
|
when:
|
||||||
|
branch: "nulo/release/v1.20"
|
||||||
|
event: "push"
|
||||||
|
|
|
@ -4,7 +4,7 @@ This changelog goes through all the changes that have been made in each release
|
||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/1.20.4) - 2023-09-08
|
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Check blocklist for emails when adding them to account (#26812) (#26831)
|
* Check blocklist for emails when adding them to account (#26812) (#26831)
|
||||||
|
|
|
@ -65,10 +65,10 @@ RUN addgroup \
|
||||||
-s /bin/bash \
|
-s /bin/bash \
|
||||||
-u 1000 \
|
-u 1000 \
|
||||||
-G git \
|
-G git \
|
||||||
git && \
|
_gitea && \
|
||||||
echo "git:*" | chpasswd -e
|
echo "_gitea:*" | chpasswd -e
|
||||||
|
|
||||||
ENV USER git
|
ENV USER _gitea
|
||||||
ENV GITEA_CUSTOM /data/gitea
|
ENV GITEA_CUSTOM /data/gitea
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
||||||
VERSION = ${GITEA_VERSION}
|
VERSION = ${GITEA_VERSION}
|
||||||
|
|
||||||
# SemVer
|
# SemVer
|
||||||
FORGEJO_VERSION := 5.0.3+0-gitea-1.20.4
|
FORGEJO_VERSION := 5.0.4+0-gitea-1.20.4
|
||||||
|
|
||||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
||||||
|
|
||||||
|
|
|
@ -1724,8 +1724,8 @@ LEVEL = Info
|
||||||
;; Session cookie name
|
;; Session cookie name
|
||||||
;COOKIE_NAME = i_like_gitea
|
;COOKIE_NAME = i_like_gitea
|
||||||
;;
|
;;
|
||||||
;; If you use session in https only, default is false
|
;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
|
||||||
;COOKIE_SECURE = false
|
;COOKIE_SECURE =
|
||||||
;;
|
;;
|
||||||
;; Session GC time interval in seconds, default is 86400 (1 day)
|
;; Session GC time interval in seconds, default is 86400 (1 day)
|
||||||
;GC_INTERVAL_TIME = 86400
|
;GC_INTERVAL_TIME = 86400
|
||||||
|
|
|
@ -443,7 +443,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
||||||
- `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating.
|
- `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating.
|
||||||
- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this.
|
- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this.
|
||||||
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
|
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
|
||||||
- `LOG_SQL`: **true**: Log the executed SQL.
|
- `LOG_SQL`: **false**: Log the executed SQL.
|
||||||
- `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed.
|
- `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed.
|
||||||
- `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred.
|
- `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred.
|
||||||
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
|
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
|
||||||
|
@ -772,7 +772,7 @@ and
|
||||||
|
|
||||||
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
|
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
|
||||||
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
|
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
|
||||||
- `COOKIE_SECURE`: **false**: Enable this to force using HTTPS for all session access.
|
- `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
|
||||||
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
|
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
|
||||||
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
|
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
|
||||||
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)
|
- `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day)
|
||||||
|
|
|
@ -98,7 +98,7 @@ menu:
|
||||||
- `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。
|
- `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。
|
||||||
- `CHARSET`: **utf8mb4**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。
|
- `CHARSET`: **utf8mb4**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。
|
||||||
- `PATH`: SQLite3 数据文件存放路径。
|
- `PATH`: SQLite3 数据文件存放路径。
|
||||||
- `LOG_SQL`: **true**: 显示生成的SQL,默认为真。
|
- `LOG_SQL`: **false**: 显示生成的SQL,默认为真。
|
||||||
- `MAX_IDLE_CONNS` **0**: 最大空闲数据库连接
|
- `MAX_IDLE_CONNS` **0**: 最大空闲数据库连接
|
||||||
- `CONN_MAX_LIFETIME` **3s**: 数据库连接最大存活时间
|
- `CONN_MAX_LIFETIME` **3s**: 数据库连接最大存活时间
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ menu:
|
||||||
|
|
||||||
- `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。
|
- `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。
|
||||||
- `PROVIDER_CONFIG`: 如果是文件,那么这里填根目录;其他的要填主机地址和端口。
|
- `PROVIDER_CONFIG`: 如果是文件,那么这里填根目录;其他的要填主机地址和端口。
|
||||||
- `COOKIE_SECURE`: 强制使用 HTTPS 作为session访问。
|
- `COOKIE_SECURE`: **_empty_**:`true` 或 `false`。启用此选项以强制在所有会话访问中使用 HTTPS。如果没有设置,当 ROOT_URL 是 https 链接的时候默认设置为 true。
|
||||||
- `GC_INTERVAL_TIME`: Session失效时间。
|
- `GC_INTERVAL_TIME`: Session失效时间。
|
||||||
|
|
||||||
## Picture (`picture`)
|
## Picture (`picture`)
|
||||||
|
|
|
@ -180,3 +180,6 @@ For events supported only by GitHub, see GitHub's [documentation](https://docs.g
|
||||||
| pull_request_review_comment | `created`, `edited` |
|
| pull_request_review_comment | `created`, `edited` |
|
||||||
| release | `published`, `edited` |
|
| release | `published`, `edited` |
|
||||||
| registry_package | `published` |
|
| registry_package | `published` |
|
||||||
|
|
||||||
|
> For `pull_request` events, in [GitHub Actions](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request), the `ref` is `refs/pull/:prNumber/merge`, which is a reference to the merge commit preview. However, Gitea has no such reference.
|
||||||
|
> Therefore, the `ref` in Gitea Actions is `refs/pull/:prNumber/head`, which points to the head of pull request rather than the preview of the merge commit.
|
||||||
|
|
|
@ -180,3 +180,6 @@ defaults:
|
||||||
| pull_request_review_comment | `created`, `edited` |
|
| pull_request_review_comment | `created`, `edited` |
|
||||||
| release | `published`, `edited` |
|
| release | `published`, `edited` |
|
||||||
| registry_package | `published` |
|
| registry_package | `published` |
|
||||||
|
|
||||||
|
> 对于 `pull_request` 事件,在 [GitHub Actions](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request) 中 `ref` 是 `refs/pull/:prNumber/merge`,它指向这个拉取请求合并提交的一个预览。但是 Gitea 没有这种 reference。
|
||||||
|
> 因此,Gitea Actions 中 `ref` 是 `refs/pull/:prNumber/head`,它指向这个拉取请求的头分支而不是合并提交的预览。
|
||||||
|
|
|
@ -153,7 +153,12 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
|
||||||
return DefaultAvatarLink()
|
return DefaultAvatarLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar)
|
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
|
||||||
|
setting.GetDefaultDisableGravatar(),
|
||||||
|
)
|
||||||
|
|
||||||
|
enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar,
|
||||||
|
setting.GetDefaultEnableFederatedAvatar(disableGravatar))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if enableFederatedAvatar && system_model.LibravatarService != nil {
|
if enableFederatedAvatar && system_model.LibravatarService != nil {
|
||||||
|
@ -174,7 +179,6 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final
|
||||||
return urlStr
|
return urlStr
|
||||||
}
|
}
|
||||||
|
|
||||||
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
|
|
||||||
if !disableGravatar {
|
if !disableGravatar {
|
||||||
// copy GravatarSourceURL, because we will modify its Path.
|
// copy GravatarSourceURL, because we will modify its Path.
|
||||||
avatarURLCopy := *system_model.GravatarSourceURL
|
avatarURLCopy := *system_model.GravatarSourceURL
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
-
|
-
|
||||||
id: 1
|
id: 1
|
||||||
setting_key: 'disable_gravatar'
|
setting_key: 'picture.disable_gravatar'
|
||||||
setting_value: 'false'
|
setting_value: 'false'
|
||||||
version: 1
|
version: 1
|
||||||
created: 1653533198
|
created: 1653533198
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 2
|
id: 2
|
||||||
setting_key: 'enable_federated_avatar'
|
setting_key: 'picture.enable_federated_avatar'
|
||||||
setting_value: 'false'
|
setting_value: 'false'
|
||||||
version: 1
|
version: 1
|
||||||
created: 1653533198
|
created: 1653533198
|
||||||
|
|
|
@ -348,7 +348,7 @@ func (c *Comment) AfterLoad(session *xorm.Session) {
|
||||||
|
|
||||||
// LoadPoster loads comment poster
|
// LoadPoster loads comment poster
|
||||||
func (c *Comment) LoadPoster(ctx context.Context) (err error) {
|
func (c *Comment) LoadPoster(ctx context.Context) (err error) {
|
||||||
if c.PosterID <= 0 || c.Poster != nil {
|
if c.Poster != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -523,9 +523,9 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
|
||||||
}, repo.Description)
|
}, repo.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
|
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
|
||||||
return template.HTML(markup.Sanitize(repo.Description))
|
return template.HTML(markup.SanitizeDescription(repo.Description))
|
||||||
}
|
}
|
||||||
return template.HTML(markup.Sanitize(desc))
|
return template.HTML(markup.SanitizeDescription(desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloneLink represents different types of clone URLs of repository.
|
// CloneLink represents different types of clone URLs of repository.
|
||||||
|
|
|
@ -94,11 +94,14 @@ func GetSetting(ctx context.Context, key string) (*Setting, error) {
|
||||||
const contextCacheKey = "system_setting"
|
const contextCacheKey = "system_setting"
|
||||||
|
|
||||||
// GetSettingWithCache returns the setting value via the key
|
// GetSettingWithCache returns the setting value via the key
|
||||||
func GetSettingWithCache(ctx context.Context, key string) (string, error) {
|
func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) {
|
||||||
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
|
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
|
||||||
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
|
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
|
||||||
res, err := GetSetting(ctx, key)
|
res, err := GetSetting(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if IsErrSettingIsNotExist(err) {
|
||||||
|
return defaultVal, nil
|
||||||
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return res.SettingValue, nil
|
return res.SettingValue, nil
|
||||||
|
@ -108,17 +111,21 @@ func GetSettingWithCache(ctx context.Context, key string) (string, error) {
|
||||||
|
|
||||||
// GetSettingBool return bool value of setting,
|
// GetSettingBool return bool value of setting,
|
||||||
// none existing keys and errors are ignored and result in false
|
// none existing keys and errors are ignored and result in false
|
||||||
func GetSettingBool(ctx context.Context, key string) bool {
|
func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) {
|
||||||
s, _ := GetSetting(ctx, key)
|
s, err := GetSetting(ctx, key)
|
||||||
if s == nil {
|
switch {
|
||||||
return false
|
case err == nil:
|
||||||
}
|
|
||||||
v, _ := strconv.ParseBool(s.SettingValue)
|
v, _ := strconv.ParseBool(s.SettingValue)
|
||||||
return v
|
return v, nil
|
||||||
|
case IsErrSettingIsNotExist(err):
|
||||||
|
return defaultVal, nil
|
||||||
|
default:
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSettingWithCacheBool(ctx context.Context, key string) bool {
|
func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool {
|
||||||
s, _ := GetSettingWithCache(ctx, key)
|
s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal))
|
||||||
v, _ := strconv.ParseBool(s)
|
v, _ := strconv.ParseBool(s)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
@ -259,52 +266,41 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
var disableGravatar bool
|
disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar())
|
||||||
disableGravatarSetting, err := GetSetting(ctx, KeyPictureDisableGravatar)
|
if err != nil {
|
||||||
if IsErrSettingIsNotExist(err) {
|
|
||||||
disableGravatar = setting_module.GetDefaultDisableGravatar()
|
|
||||||
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else {
|
|
||||||
disableGravatar = disableGravatarSetting.GetValueBool()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var enableFederatedAvatar bool
|
enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar))
|
||||||
enableFederatedAvatarSetting, err := GetSetting(ctx, KeyPictureEnableFederatedAvatar)
|
if err != nil {
|
||||||
if IsErrSettingIsNotExist(err) {
|
|
||||||
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
|
|
||||||
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else {
|
|
||||||
enableFederatedAvatar = disableGravatarSetting.GetValueBool()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting_module.OfflineMode {
|
if setting_module.OfflineMode {
|
||||||
disableGravatar = true
|
if !disableGravatar {
|
||||||
enableFederatedAvatar = false
|
|
||||||
if !GetSettingBool(ctx, KeyPictureDisableGravatar) {
|
|
||||||
if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil {
|
if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil {
|
||||||
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err)
|
return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if GetSettingBool(ctx, KeyPictureEnableFederatedAvatar) {
|
disableGravatar = true
|
||||||
|
|
||||||
|
if enableFederatedAvatar {
|
||||||
if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil {
|
if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil {
|
||||||
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
|
return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
enableFederatedAvatar = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableFederatedAvatar || !disableGravatar {
|
if enableFederatedAvatar || !disableGravatar {
|
||||||
var err error
|
var err error
|
||||||
GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
|
GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
|
return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() {
|
if GravatarSourceURL != nil && enableFederatedAvatar {
|
||||||
LibravatarService = libravatar.New()
|
LibravatarService = libravatar.New()
|
||||||
if GravatarSourceURL.Scheme == "https" {
|
if GravatarSourceURL.Scheme == "https" {
|
||||||
LibravatarService.SetUseHTTPS(true)
|
LibravatarService.SetUseHTTPS(true)
|
||||||
|
|
|
@ -67,7 +67,9 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||||
useLocalAvatar := false
|
useLocalAvatar := false
|
||||||
autoGenerateAvatar := false
|
autoGenerateAvatar := false
|
||||||
|
|
||||||
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
|
disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar,
|
||||||
|
setting.GetDefaultDisableGravatar(),
|
||||||
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case u.UseCustomAvatar:
|
case u.UseCustomAvatar:
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
// any modification to the underlying policies once it's been created.
|
// any modification to the underlying policies once it's been created.
|
||||||
type Sanitizer struct {
|
type Sanitizer struct {
|
||||||
defaultPolicy *bluemonday.Policy
|
defaultPolicy *bluemonday.Policy
|
||||||
|
descriptionPolicy *bluemonday.Policy
|
||||||
rendererPolicies map[string]*bluemonday.Policy
|
rendererPolicies map[string]*bluemonday.Policy
|
||||||
init sync.Once
|
init sync.Once
|
||||||
}
|
}
|
||||||
|
@ -41,6 +42,7 @@ func NewSanitizer() {
|
||||||
func InitializeSanitizer() {
|
func InitializeSanitizer() {
|
||||||
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
||||||
sanitizer.defaultPolicy = createDefaultPolicy()
|
sanitizer.defaultPolicy = createDefaultPolicy()
|
||||||
|
sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
|
||||||
|
|
||||||
for name, renderer := range renderers {
|
for name, renderer := range renderers {
|
||||||
sanitizerRules := renderer.SanitizerRules()
|
sanitizerRules := renderer.SanitizerRules()
|
||||||
|
@ -161,6 +163,27 @@ func createDefaultPolicy() *bluemonday.Policy {
|
||||||
return policy
|
return policy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||||
|
// repository descriptions.
|
||||||
|
func createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||||
|
policy := bluemonday.NewPolicy()
|
||||||
|
|
||||||
|
// Allow italics and bold.
|
||||||
|
policy.AllowElements("i", "b", "em", "strong")
|
||||||
|
|
||||||
|
// Allow code.
|
||||||
|
policy.AllowElements("code")
|
||||||
|
|
||||||
|
// Allow links
|
||||||
|
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||||
|
|
||||||
|
// Allow classes for emojis
|
||||||
|
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||||
|
policy.AllowAttrs("aria-label").OnElements("span")
|
||||||
|
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule.AllowDataURIImages {
|
if rule.AllowDataURIImages {
|
||||||
|
@ -176,6 +199,12 @@ func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||||
|
func SanitizeDescription(s string) string {
|
||||||
|
NewSanitizer()
|
||||||
|
return sanitizer.descriptionPolicy.Sanitize(s)
|
||||||
|
}
|
||||||
|
|
||||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||||
func Sanitize(s string) string {
|
func Sanitize(s string) string {
|
||||||
NewSanitizer()
|
NewSanitizer()
|
||||||
|
|
|
@ -73,6 +73,28 @@ func Test_Sanitizer(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDescriptionSanitizer(t *testing.T) {
|
||||||
|
NewSanitizer()
|
||||||
|
|
||||||
|
testCases := []string{
|
||||||
|
`<h1>Title</h1>`, `Title`,
|
||||||
|
`<img src='img.png' alt='image'>`, ``,
|
||||||
|
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||||
|
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||||
|
`<br>`, ``,
|
||||||
|
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
|
||||||
|
`<mark>Important!</mark>`, `Important!`,
|
||||||
|
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||||
|
`<input type="hidden">`, ``,
|
||||||
|
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||||
|
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(testCases); i += 2 {
|
||||||
|
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSanitizeNonEscape(t *testing.T) {
|
func TestSanitizeNonEscape(t *testing.T) {
|
||||||
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,9 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(ctx context.Context, doer *u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mailNotifier) NotifyPullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
|
func (m *mailNotifier) NotifyPullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
|
||||||
|
if err := comment.Review.LoadReviewer(ctx); err != nil {
|
||||||
|
log.Error("Error in PullReviewDismiss while loading reviewer for issue[%d], review[%d] and reviewer[%d]: %v", review.Issue.ID, comment.Review.ID, comment.Review.ReviewerID, err)
|
||||||
|
}
|
||||||
if err := mailer.MailParticipantsComment(ctx, comment, activities_model.ActionPullReviewDismissed, review.Issue, nil); err != nil {
|
if err := mailer.MailParticipantsComment(ctx, comment, activities_model.ActionPullReviewDismissed, review.Issue, nil); err != nil {
|
||||||
log.Error("MailParticipantsComment: %v", err)
|
log.Error("MailParticipantsComment: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/avatars"
|
"code.gitea.io/gitea/models/avatars"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -34,42 +35,36 @@ type PushCommits struct {
|
||||||
HeadCommit *PushCommit
|
HeadCommit *PushCommit
|
||||||
CompareURL string
|
CompareURL string
|
||||||
Len int
|
Len int
|
||||||
|
|
||||||
avatars map[string]string
|
|
||||||
emailUsers map[string]*user_model.User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPushCommits creates a new PushCommits object.
|
// NewPushCommits creates a new PushCommits object.
|
||||||
func NewPushCommits() *PushCommits {
|
func NewPushCommits() *PushCommits {
|
||||||
return &PushCommits{
|
return &PushCommits{}
|
||||||
avatars: make(map[string]string),
|
|
||||||
emailUsers: make(map[string]*user_model.User),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
|
// toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object.
|
||||||
func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
|
func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) {
|
||||||
var err error
|
var err error
|
||||||
authorUsername := ""
|
authorUsername := ""
|
||||||
author, ok := pc.emailUsers[commit.AuthorEmail]
|
author, ok := emailUsers[commit.AuthorEmail]
|
||||||
if !ok {
|
if !ok {
|
||||||
author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail)
|
author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
authorUsername = author.Name
|
authorUsername = author.Name
|
||||||
pc.emailUsers[commit.AuthorEmail] = author
|
emailUsers[commit.AuthorEmail] = author
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authorUsername = author.Name
|
authorUsername = author.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
committerUsername := ""
|
committerUsername := ""
|
||||||
committer, ok := pc.emailUsers[commit.CommitterEmail]
|
committer, ok := emailUsers[commit.CommitterEmail]
|
||||||
if !ok {
|
if !ok {
|
||||||
committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail)
|
committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// TODO: check errors other than email not found.
|
// TODO: check errors other than email not found.
|
||||||
committerUsername = committer.Name
|
committerUsername = committer.Name
|
||||||
pc.emailUsers[commit.CommitterEmail] = committer
|
emailUsers[commit.CommitterEmail] = committer
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
committerUsername = committer.Name
|
committerUsername = committer.Name
|
||||||
|
@ -107,11 +102,10 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
|
||||||
commits := make([]*api.PayloadCommit, len(pc.Commits))
|
commits := make([]*api.PayloadCommit, len(pc.Commits))
|
||||||
var headCommit *api.PayloadCommit
|
var headCommit *api.PayloadCommit
|
||||||
|
|
||||||
if pc.emailUsers == nil {
|
emailUsers := make(map[string]*user_model.User)
|
||||||
pc.emailUsers = make(map[string]*user_model.User)
|
|
||||||
}
|
|
||||||
for i, commit := range pc.Commits {
|
for i, commit := range pc.Commits {
|
||||||
apiCommit, err := pc.toAPIPayloadCommit(ctx, repoPath, repoLink, commit)
|
apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -123,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
|
||||||
}
|
}
|
||||||
if pc.HeadCommit != nil && headCommit == nil {
|
if pc.HeadCommit != nil && headCommit == nil {
|
||||||
var err error
|
var err error
|
||||||
headCommit, err = pc.toAPIPayloadCommit(ctx, repoPath, repoLink, pc.HeadCommit)
|
headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -134,35 +128,21 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi
|
||||||
// AvatarLink tries to match user in database with e-mail
|
// AvatarLink tries to match user in database with e-mail
|
||||||
// in order to show custom avatar, and falls back to general avatar link.
|
// in order to show custom avatar, and falls back to general avatar link.
|
||||||
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
|
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
|
||||||
if pc.avatars == nil {
|
|
||||||
pc.avatars = make(map[string]string)
|
|
||||||
}
|
|
||||||
avatar, ok := pc.avatars[email]
|
|
||||||
if ok {
|
|
||||||
return avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
|
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
|
||||||
|
|
||||||
u, ok := pc.emailUsers[email]
|
v, _ := cache.GetWithContextCache(ctx, "push_commits", email, func() (string, error) {
|
||||||
if !ok {
|
u, err := user_model.GetUserByEmail(ctx, email)
|
||||||
var err error
|
|
||||||
u, err = user_model.GetUserByEmail(ctx, email)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(ctx, email, size)
|
|
||||||
if !user_model.IsErrUserNotExist(err) {
|
if !user_model.IsErrUserNotExist(err) {
|
||||||
log.Error("GetUserByEmail: %v", err)
|
log.Error("GetUserByEmail: %v", err)
|
||||||
return ""
|
return "", err
|
||||||
}
|
}
|
||||||
} else {
|
return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil
|
||||||
pc.emailUsers[email] = u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if u != nil {
|
|
||||||
pc.avatars[email] = u.AvatarLinkWithSize(ctx, size)
|
|
||||||
}
|
}
|
||||||
|
return u.AvatarLinkWithSize(ctx, size), nil
|
||||||
|
})
|
||||||
|
|
||||||
return pc.avatars[email]
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitToPushCommit transforms a git.Commit to PushCommit type.
|
// CommitToPushCommit transforms a git.Commit to PushCommit type.
|
||||||
|
@ -189,7 +169,5 @@ func GitToPushCommits(gitCommits []*git.Commit) *PushCommits {
|
||||||
HeadCommit: nil,
|
HeadCommit: nil,
|
||||||
CompareURL: "",
|
CompareURL: "",
|
||||||
Len: len(commits),
|
Len: len(commits),
|
||||||
avatars: make(map[string]string),
|
|
||||||
emailUsers: make(map[string]*user_model.User),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,11 +103,9 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) {
|
||||||
assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified)
|
assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified)
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableGravatar(t *testing.T) {
|
func initGravatarSource(t *testing.T) {
|
||||||
err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
setting.GravatarSource = "https://secure.gravatar.com/avatar"
|
setting.GravatarSource = "https://secure.gravatar.com/avatar"
|
||||||
err = system_model.Init(db.DefaultContext)
|
err := system_model.Init(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +132,7 @@ func TestPushCommits_AvatarLink(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
enableGravatar(t)
|
initGravatarSource(t)
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
|
"https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
|
||||||
|
|
|
@ -175,9 +175,16 @@ func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configProviderLoadOptions() ini.LoadOptions {
|
||||||
|
return ini.LoadOptions{
|
||||||
|
KeyValueDelimiterOnWrite: " = ",
|
||||||
|
IgnoreContinuation: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewConfigProviderFromData this function is mainly for testing purpose
|
// NewConfigProviderFromData this function is mainly for testing purpose
|
||||||
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
||||||
cfg, err := ini.Load(strings.NewReader(configContent))
|
cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -191,7 +198,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
||||||
// NewConfigProviderFromFile load configuration from file.
|
// NewConfigProviderFromFile load configuration from file.
|
||||||
// NOTE: do not print any log except error.
|
// NOTE: do not print any log except error.
|
||||||
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
|
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
|
||||||
cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "})
|
cfg := ini.Empty(configProviderLoadOptions())
|
||||||
loadedFromEmpty := true
|
loadedFromEmpty := true
|
||||||
|
|
||||||
if file != "" {
|
if file != "" {
|
||||||
|
@ -344,6 +351,7 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro
|
||||||
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
||||||
IgnoreInlineComment: true,
|
IgnoreInlineComment: true,
|
||||||
UnescapeValueCommentSymbols: true,
|
UnescapeValueCommentSymbols: true,
|
||||||
|
IgnoreContinuation: true,
|
||||||
}, source, others...)
|
}, source, others...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to load locale ini: %w", err)
|
return nil, fmt.Errorf("unable to load locale ini: %w", err)
|
||||||
|
|
|
@ -30,6 +30,16 @@ key = 123
|
||||||
secSub := cfg.Section("foo.bar.xxx")
|
secSub := cfg.Section("foo.bar.xxx")
|
||||||
assert.Equal(t, "123", secSub.Key("key").String())
|
assert.Equal(t, "123", secSub.Key("key").String())
|
||||||
})
|
})
|
||||||
|
t.Run("TrailingSlash", func(t *testing.T) {
|
||||||
|
cfg, _ := NewConfigProviderFromData(`
|
||||||
|
[foo]
|
||||||
|
key = E:\
|
||||||
|
xxx = yyy
|
||||||
|
`)
|
||||||
|
sec := cfg.Section("foo")
|
||||||
|
assert.Equal(t, "E:\\", sec.Key("key").String())
|
||||||
|
assert.Equal(t, "yyy", sec.Key("xxx").String())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigProviderHelper(t *testing.T) {
|
func TestConfigProviderHelper(t *testing.T) {
|
||||||
|
|
|
@ -50,7 +50,7 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||||
}
|
}
|
||||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||||
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
|
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
|
||||||
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(false)
|
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
|
||||||
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
|
||||||
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
|
||||||
SessionConfig.Domain = sec.Key("DOMAIN").String()
|
SessionConfig.Domain = sec.Key("DOMAIN").String()
|
||||||
|
|
|
@ -104,7 +104,7 @@ func NewFuncMap() template.FuncMap {
|
||||||
return setting.AssetVersion
|
return setting.AssetVersion
|
||||||
},
|
},
|
||||||
"DisableGravatar": func(ctx context.Context) bool {
|
"DisableGravatar": func(ctx context.Context) bool {
|
||||||
return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar)
|
return system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, setting.GetDefaultDisableGravatar())
|
||||||
},
|
},
|
||||||
"DefaultShowFullName": func() bool {
|
"DefaultShowFullName": func() bool {
|
||||||
return setting.UI.DefaultShowFullName
|
return setting.UI.DefaultShowFullName
|
||||||
|
|
|
@ -785,7 +785,7 @@ func CompareDiff(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["IsRepoToolbarCommits"] = true
|
ctx.Data["IsRepoToolbarCommits"] = true
|
||||||
ctx.Data["IsDiffCompare"] = true
|
ctx.Data["IsDiffCompare"] = true
|
||||||
templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
|
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
|
||||||
|
|
||||||
if len(templateErrs) > 0 {
|
if len(templateErrs) > 0 {
|
||||||
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
|
||||||
|
|
|
@ -804,10 +804,11 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error {
|
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
|
||||||
|
func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
|
||||||
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
templateCandidates := make([]string, 0, 1+len(possibleFiles))
|
templateCandidates := make([]string, 0, 1+len(possibleFiles))
|
||||||
|
@ -870,20 +871,15 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
||||||
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
|
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
|
||||||
ctx.Data["Reference"] = template.Ref
|
ctx.Data["Reference"] = template.Ref
|
||||||
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
||||||
return templateErrs
|
return true, templateErrs
|
||||||
}
|
}
|
||||||
return templateErrs
|
return false, templateErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssue render creating issue page
|
// NewIssue render creating issue page
|
||||||
func NewIssue(ctx *context.Context) {
|
func NewIssue(ctx *context.Context) {
|
||||||
issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
if !issueConfig.BlankIssuesEnabled && hasTemplates {
|
|
||||||
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
|
|
||||||
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
|
||||||
ctx.Data["PageIsIssueList"] = true
|
ctx.Data["PageIsIssueList"] = true
|
||||||
|
@ -930,7 +926,8 @@ func NewIssue(ctx *context.Context) {
|
||||||
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
|
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
|
||||||
|
|
||||||
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
|
||||||
if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
|
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
|
||||||
|
if len(errs) > 0 {
|
||||||
for k, v := range errs {
|
for k, v := range errs {
|
||||||
templateErrs[k] = v
|
templateErrs[k] = v
|
||||||
}
|
}
|
||||||
|
@ -945,6 +942,12 @@ func NewIssue(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
|
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
|
||||||
|
|
||||||
|
if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded {
|
||||||
|
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssueNew)
|
ctx.HTML(http.StatusOK, tplIssueNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,10 +228,10 @@ func Profile(ctx *context.Context) {
|
||||||
switch tab {
|
switch tab {
|
||||||
case "followers":
|
case "followers":
|
||||||
ctx.Data["Cards"] = followers
|
ctx.Data["Cards"] = followers
|
||||||
total = int(count)
|
total = int(numFollowers)
|
||||||
case "following":
|
case "following":
|
||||||
ctx.Data["Cards"] = following
|
ctx.Data["Cards"] = following
|
||||||
total = int(count)
|
total = int(numFollowing)
|
||||||
case "activity":
|
case "activity":
|
||||||
date := ctx.FormString("date")
|
date := ctx.FormString("date")
|
||||||
items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||||
|
|
|
@ -249,8 +249,8 @@ func TestPrepareWikiFileName(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
||||||
defer gitRepo.Close()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -301,8 +301,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
|
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
|
||||||
defer gitRepo.Close()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
|
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
|
||||||
assert.False(t, existence)
|
assert.False(t, existence)
|
||||||
|
|
|
@ -136,6 +136,43 @@ func TestAPIGetComment(t *testing.T) {
|
||||||
assert.Equal(t, expect.Created.Unix(), apiComment.Created.Unix())
|
assert.Equal(t, expect.Created.Unix(), apiComment.Created.Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIGetSystemUserComment(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
for _, systemUser := range []*user_model.User{
|
||||||
|
user_model.NewGhostUser(),
|
||||||
|
user_model.NewActionsUser(),
|
||||||
|
} {
|
||||||
|
body := fmt.Sprintf("Hello %s", systemUser.Name)
|
||||||
|
comment, err := issues_model.CreateComment(db.DefaultContext, &issues_model.CreateCommentOptions{
|
||||||
|
Type: issues_model.CommentTypeComment,
|
||||||
|
Doer: systemUser,
|
||||||
|
Repo: repo,
|
||||||
|
Issue: issue,
|
||||||
|
Content: body,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var apiComment api.Comment
|
||||||
|
DecodeJSON(t, resp, &apiComment)
|
||||||
|
|
||||||
|
if assert.NotNil(t, apiComment.Poster) {
|
||||||
|
if assert.Equal(t, systemUser.ID, apiComment.Poster.ID) {
|
||||||
|
assert.NoError(t, comment.LoadPoster(db.DefaultContext))
|
||||||
|
assert.Equal(t, systemUser.Name, apiComment.Poster.UserName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, body, apiComment.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIEditComment(t *testing.T) {
|
func TestAPIEditComment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
const newCommentBody = "This is the new comment body"
|
const newCommentBody = "This is the new comment body"
|
||||||
|
|
Loading…
Reference in a new issue