Compare commits

...

15 commits

Author SHA1 Message Date
Cat /dev/Nulo d803ad1663 nulo: woodpecker CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-09-21 10:55:51 -03:00
Cat /dev/Nulo ebf42af71c Dockerfile: rename user to _gitea instead of git 2023-09-21 10:55:51 -03:00
Earl Warren 99a93025d2
[SEMVER] 5.0.4+0-gitea-1.20.4 2023-09-20 12:51:52 +02:00
Giteabot 0d86ea0c43
Improve actions docs related to pull_request event (#27126) (#27145)
Backport #27126 by @Zettat123

Related to #27039

The `ref` property in Gitea Actions is different from GitHub Actions.
This PR improves the documentation to explain the difference.

Co-authored-by: Zettat123 <zettat123@gmail.com>
(cherry picked from commit 7a99c7b83c0c1b6002ab6ff43000cd928b791643)
2023-09-20 12:50:46 +02:00
Giteabot c041114a20
fix pagination for followers and following (#27127) (#27138)
Backport #27127 by @earl-warren

- Use the correct total amount for pagination. Thereby correctly show
the pagination bare when there's more than one page of
followers/followings.

Refs: https://codeberg.org/forgejo/forgejo/pulls/1477

(cherry picked from commit c1a136318be3bf72511bed108f2d67f2cf34e1b8)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Gusted <postmaster@gusted.xyz>
(cherry picked from commit 1d6e5c8e5862e634081c943f346003c36e47415f)
2023-09-20 12:50:46 +02:00
Giteabot f54189092f
services/wiki: Close() after error handling (#27129) (#27137)
Backport #27129 by @earl-warren

Refs: https://codeberg.org/forgejo/forgejo/pulls/1385

Signed-off-by: Lars Lehtonen <lars.lehtonen@gmail.com>
(cherry picked from commit 589e7d346f51de4a0e2c461b220c8cad34133b2f)

Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
Co-authored-by: Lars Lehtonen <lars.lehtonen@gmail.com>
(cherry picked from commit 882e465c3a4c60a47b9f8a2f58e3cc88c19ed641)
2023-09-20 12:50:46 +02:00
Giteabot 64a418dfc7
Fix issue templates when blank isses are disabled (#27061) (#27082)
Backport #27061 by @JakobDev

Fixes #27060

Co-authored-by: JakobDev <jakobdev@gmx.de>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
(cherry picked from commit b139234fa8d7b9d52f134117bbac7dce53e4914b)
2023-09-20 12:50:46 +02:00
sebastian-sauer 3ea5384241
Load reviewer before sending notification (#27063) (#27064)
Fixes #27035

(cherry picked from commit d8b39324d71112f0d8c2e38b2aa92fdccec536ef)
2023-09-20 12:50:46 +02:00
Lunny Xiao 745b45406d
Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017)
backport #26991

Unfortunately, when a system setting hasn't been stored in the database,
it cannot be cached.
Meanwhile, this PR also uses context cache for push email avatar display
which should avoid to read user table via email address again and again.

According to my local test, this should reduce dashboard elapsed time
from 150ms -> 80ms .

(cherry picked from commit 9df573bddcc01bc8c3ab495629678ad577071831)
2023-09-20 12:50:46 +02:00
wxiaoguang 8f6d442a04
Use secure cookie for HTTPS sites (#26999) (#27013)
Backport #26999

If the AppURL(ROOT_URL) is an HTTPS URL, then the COOKIE_SECURE's
default value should be true.

And, if a user visits an "http" site with "https" AppURL, they won't be
able to login, and they should have been warned. The only problem is
that the "language" can't be set either in such case, while I think it
is not a serious problem, and it could be fixed easily if needed.

(cherry picked from commit b0a405c5fad2055976747a1c8b2c48dfe2750c9f)
2023-09-20 12:50:46 +02:00
Infinoid 2e9fa11bb3
Correct the database.LOG_SQL default value in config cheat sheet (#26997) (#27002)
This is a manual backport of #26997 to v1.20.

(cherry picked from commit 3c53740244e175a253411c8fedcc1d3fff19ea7a)
2023-09-20 12:50:46 +02:00
Giteabot 957a64d91a
Fix INI parsing for value with trailing slash (#26995) (#27001)
Backport #26995 by @wxiaoguang

Fix #26977 (a temp fix)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
(cherry picked from commit da7d7e60d800a9b13ed1341ef99bf3db062ed942)
2023-09-20 12:50:46 +02:00
Lunny Xiao 56a17f3565
Fix changelog typo (#26973)
(cherry picked from commit e502be46f3ac5f1075abd96bd9b326a4c1f7e96f)
2023-09-20 12:50:46 +02:00
Gusted 4b9a473e12
[GITEA] Use restricted sanitizer for repository description
- Backport of https://codeberg.org/forgejo/forgejo/pulls/1433
  - Currently the repository description uses the same sanitizer as a
normal markdown document. This means that element such as heading and
images are allowed and can be abused.
  - Create a minimal restricted sanitizer for the repository description,
which only allows what the postprocessor currently allows, which are
links and emojis.
  - Added unit testing.
  - Resolves https://codeberg.org/forgejo/forgejo/issues/1202
  - Resolves https://codeberg.org/Codeberg/Community/issues/1122

(cherry picked from commit a8afa4cd181d7c31f73d6a8fae4c6a4b9622a425)
2023-09-13 17:17:37 +02:00
Earl Warren 5aad8a6918
[GITEA] enable system users for comment.LoadPoster
System users (Ghost, ActionsUser, etc) have a negative id and may be
the author of a comment, either because it was created by a now
deleted user or via an action using a transient token.

The GetPossibleUserByID function has special cases related to system
users and will not fail if given a negative id.

Refs: https://codeberg.org/forgejo/forgejo/issues/1425
(cherry picked from commit 97667e06b384d834a04eaa05e8f91563481709b1)
2023-09-12 11:02:07 +02:00
29 changed files with 230 additions and 119 deletions

15
.woodpecker.yml Normal file
View 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"

View file

@ -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)

View file

@ -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"]

View file

@ -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)"

View file

@ -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

View file

@ -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)

View file

@ -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`)

View file

@ -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.

View file

@ -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`,它指向这个拉取请求的头分支而不是合并提交的预览。

View file

@ -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

View file

@ -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

View file

@ -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
} }

View file

@ -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.

View file

@ -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)
return v, nil
case IsErrSettingIsNotExist(err):
return defaultVal, nil
default:
return false, err
} }
v, _ := strconv.ParseBool(s.SettingValue)
return v
} }
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)

View file

@ -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:

View file

@ -18,9 +18,10 @@ import (
// Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow // Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow
// 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
rendererPolicies map[string]*bluemonday.Policy descriptionPolicy *bluemonday.Policy
init sync.Once rendererPolicies map[string]*bluemonday.Policy
init sync.Once
} }
var ( var (
@ -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()

View file

@ -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>&lt;script&gt;alert(document.domain)&lt;/script&gt;</scrİpt>" descStr := "<scrİpt>&lt;script&gt;alert(document.domain)&lt;/script&gt;</scrİpt>"

View file

@ -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)
} }

View file

@ -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
} }
} return u.AvatarLinkWithSize(ctx, size), nil
if u != nil { })
pc.avatars[email] = u.AvatarLinkWithSize(ctx, size)
}
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),
} }
} }

View file

@ -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),

View file

@ -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)

View file

@ -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) {

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)
} }

View file

@ -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{

View file

@ -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)

View file

@ -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"