mirror of
https://github.com/go-gitea/gitea.git
synced 2024-11-24 02:59:42 +08:00
Merge branch 'main' into lunny/repo_dep_org
This commit is contained in:
commit
83f9f38bac
@ -642,7 +642,7 @@ rules:
|
||||
no-this-before-super: [2]
|
||||
no-throw-literal: [2]
|
||||
no-undef-init: [2]
|
||||
no-undef: [2, {typeof: true}]
|
||||
no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes
|
||||
no-undefined: [0]
|
||||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
|
@ -63,3 +63,4 @@ Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
|
||||
Yu Liu <1240335630@qq.com> (@HEREYUA)
|
||||
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
|
||||
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
|
||||
hiifong <i@hiif.ong> (@hiifong)
|
||||
|
@ -1944,6 +1944,13 @@ LEVEL = Info
|
||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_SECRET_ACCESS_KEY =
|
||||
;;
|
||||
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
|
||||
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
|
||||
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
|
||||
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
|
||||
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
|
||||
;MINIO_IAM_ENDPOINT =
|
||||
;;
|
||||
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_BUCKET = gitea
|
||||
;;
|
||||
@ -2688,6 +2695,13 @@ LEVEL = Info
|
||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_SECRET_ACCESS_KEY =
|
||||
;;
|
||||
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
|
||||
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
|
||||
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
|
||||
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
|
||||
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
|
||||
;MINIO_IAM_ENDPOINT =
|
||||
;;
|
||||
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_BUCKET = gitea
|
||||
;;
|
||||
|
@ -68,7 +68,8 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
|
||||
|
||||
var candidateCollations []string
|
||||
if x.Dialect().URI().DBType == schemas.MYSQL {
|
||||
if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil {
|
||||
_, err = x.SQL("SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", setting.Database.Name).Get(&res.DatabaseCollation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.IsCollationCaseSensitive = func(s string) bool {
|
||||
|
@ -1,3 +1,22 @@
|
||||
-
|
||||
id: 46
|
||||
attempt: 3
|
||||
runner_id: 1
|
||||
status: 3 # 3 is the status code for "cancelled"
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 4
|
||||
owner_id: 1
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
|
||||
token_salt: eeeeeeee
|
||||
token_last_eight: eeeeeeee
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 47
|
||||
job_id: 192
|
||||
|
@ -1,7 +1,7 @@
|
||||
-
|
||||
id: 1
|
||||
setting_key: 'picture.disable_gravatar'
|
||||
setting_value: 'false'
|
||||
setting_value: 'true'
|
||||
version: 1
|
||||
created: 1653533198
|
||||
updated: 1653533198
|
||||
|
@ -23,9 +23,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar1
|
||||
avatar: ""
|
||||
avatar_email: user1@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -60,8 +60,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar2
|
||||
avatar: ""
|
||||
avatar_email: user2@example.com
|
||||
# cause a random avatar to be generated when referenced for test purposes
|
||||
use_custom_avatar: false
|
||||
num_followers: 2
|
||||
num_following: 1
|
||||
@ -97,9 +98,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar3
|
||||
avatar: ""
|
||||
avatar_email: org3@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -134,9 +135,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar4
|
||||
avatar: ""
|
||||
avatar_email: user4@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 1
|
||||
num_stars: 0
|
||||
@ -171,9 +172,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: false
|
||||
prohibit_login: false
|
||||
avatar: avatar5
|
||||
avatar: ""
|
||||
avatar_email: user5@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -208,9 +209,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar6
|
||||
avatar: ""
|
||||
avatar_email: org6@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -245,9 +246,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar7
|
||||
avatar: ""
|
||||
avatar_email: org7@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -282,9 +283,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar8
|
||||
avatar: ""
|
||||
avatar_email: user8@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 1
|
||||
num_following: 1
|
||||
num_stars: 0
|
||||
@ -319,9 +320,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar9
|
||||
avatar: ""
|
||||
avatar_email: user9@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -332,6 +333,7 @@
|
||||
repo_admin_change_team_access: false
|
||||
theme: ""
|
||||
keep_activity_private: false
|
||||
created_unix: 1730468968
|
||||
|
||||
-
|
||||
id: 10
|
||||
@ -356,9 +358,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar10
|
||||
avatar: ""
|
||||
avatar_email: user10@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 2
|
||||
@ -393,9 +395,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar11
|
||||
avatar: ""
|
||||
avatar_email: user11@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -430,9 +432,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar12
|
||||
avatar: ""
|
||||
avatar_email: user12@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -467,9 +469,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar13
|
||||
avatar: ""
|
||||
avatar_email: user13@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -504,9 +506,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar14
|
||||
avatar: ""
|
||||
avatar_email: user13@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -541,9 +543,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar15
|
||||
avatar: ""
|
||||
avatar_email: user15@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -578,9 +580,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar16
|
||||
avatar: ""
|
||||
avatar_email: user16@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -615,9 +617,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar17
|
||||
avatar: ""
|
||||
avatar_email: org17@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -652,9 +654,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar18
|
||||
avatar: ""
|
||||
avatar_email: user18@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -689,9 +691,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar19
|
||||
avatar: ""
|
||||
avatar_email: org19@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -726,9 +728,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar20
|
||||
avatar: ""
|
||||
avatar_email: user20@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -763,9 +765,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar21
|
||||
avatar: ""
|
||||
avatar_email: user21@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -800,9 +802,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar22
|
||||
avatar: ""
|
||||
avatar_email: limited_org@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -837,9 +839,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar23
|
||||
avatar: ""
|
||||
avatar_email: privated_org@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -874,9 +876,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar24
|
||||
avatar: ""
|
||||
avatar_email: user24@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -911,9 +913,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar25
|
||||
avatar: ""
|
||||
avatar_email: org25@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -948,9 +950,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar26
|
||||
avatar: ""
|
||||
avatar_email: org26@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -985,9 +987,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar27
|
||||
avatar: ""
|
||||
avatar_email: user27@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1022,9 +1024,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar28
|
||||
avatar: ""
|
||||
avatar_email: user28@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1059,9 +1061,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar29
|
||||
avatar: ""
|
||||
avatar_email: user29@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1096,9 +1098,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar29
|
||||
avatar: ""
|
||||
avatar_email: user30@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1133,9 +1135,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar31
|
||||
avatar: ""
|
||||
avatar_email: user31@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 1
|
||||
num_stars: 0
|
||||
@ -1170,9 +1172,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar32
|
||||
avatar: ""
|
||||
avatar_email: user30@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1207,9 +1209,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar33
|
||||
avatar: ""
|
||||
avatar_email: user33@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 1
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1245,7 +1247,7 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: false
|
||||
prohibit_login: false
|
||||
avatar: avatar34
|
||||
avatar: ""
|
||||
avatar_email: user34@example.com
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
@ -1282,9 +1284,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar35
|
||||
avatar: ""
|
||||
avatar_email: private_org35@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1319,9 +1321,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar22
|
||||
avatar: ""
|
||||
avatar_email: abcde@gitea.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1356,9 +1358,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: true
|
||||
avatar: avatar29
|
||||
avatar: ""
|
||||
avatar_email: user37@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1393,9 +1395,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar38
|
||||
avatar: ""
|
||||
avatar_email: user38@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1430,9 +1432,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar39
|
||||
avatar: ""
|
||||
avatar_email: user39@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1467,9 +1469,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar40
|
||||
avatar: ""
|
||||
avatar_email: user40@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1504,9 +1506,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar41
|
||||
avatar: ""
|
||||
avatar_email: org41@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
@ -1541,9 +1543,9 @@
|
||||
allow_import_local: false
|
||||
allow_create_organization: true
|
||||
prohibit_login: false
|
||||
avatar: avatar42
|
||||
avatar: ""
|
||||
avatar_email: org42@example.com
|
||||
use_custom_avatar: false
|
||||
use_custom_avatar: true
|
||||
num_followers: 0
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
|
@ -112,14 +112,12 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
}
|
||||
|
||||
var err error
|
||||
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Repo: issue.Repo,
|
||||
Links: markup.Links{
|
||||
Base: issue.Repo.Link(),
|
||||
},
|
||||
Metas: issue.Repo.ComposeMetas(ctx),
|
||||
}, comment.Content); err != nil {
|
||||
rctx := markup.NewRenderContext(ctx).
|
||||
WithRepoFacade(issue.Repo).
|
||||
WithLinks(markup.Links{Base: issue.Repo.Link()}).
|
||||
WithMetas(issue.Repo.ComposeMetas(ctx))
|
||||
if comment.RenderedContent, err = markdown.RenderString(rctx,
|
||||
comment.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
)
|
||||
|
||||
// TeamRepo represents an team-repository relation.
|
||||
@ -57,3 +58,16 @@ func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode per
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Join("INNER", "team_unit", "team_unit.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_unit.type = ?", unitType).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
}
|
||||
|
31
models/organization/team_repo_test.go
Normal file
31
models/organization/team_repo_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
||||
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 2) {
|
||||
assert.EqualValues(t, 21, teams[0].ID)
|
||||
assert.EqualValues(t, 22, teams[1].ID)
|
||||
}
|
||||
}
|
@ -54,21 +54,6 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
|
||||
return &forkedRepo, nil
|
||||
}
|
||||
|
||||
// GetForks returns all the forks of the repository
|
||||
func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
var forks []*Repository
|
||||
if listOptions.Page == 0 {
|
||||
forks = make([]*Repository, 0, repo.NumForks)
|
||||
} else {
|
||||
forks = make([]*Repository, 0, listOptions.PageSize)
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
|
||||
}
|
||||
|
||||
// IncrementRepoForkNum increment repository fork number
|
||||
func IncrementRepoForkNum(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID)
|
||||
|
@ -9,15 +9,13 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrPushMirrorNotExist mirror does not exist error
|
||||
var ErrPushMirrorNotExist = util.NewNotExistErrorf("PushMirror does not exist")
|
||||
|
||||
// PushMirror represents mirror information of a repository.
|
||||
type PushMirror struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
@ -96,26 +94,46 @@ func DeletePushMirrors(ctx context.Context, opts PushMirrorOptions) error {
|
||||
return util.NewInvalidArgumentErrorf("repoID required and must be set")
|
||||
}
|
||||
|
||||
type findPushMirrorOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
SyncOnCommit optional.Option[bool]
|
||||
}
|
||||
|
||||
func (opts findPushMirrorOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.SyncOnCommit.Has() {
|
||||
cond = cond.And(builder.Eq{"sync_on_commit": opts.SyncOnCommit.Value()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// GetPushMirrorsByRepoID returns push-mirror information of a repository.
|
||||
func GetPushMirrorsByRepoID(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*PushMirror, int64, error) {
|
||||
sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
mirrors := make([]*PushMirror, 0, listOptions.PageSize)
|
||||
count, err := sess.FindAndCount(&mirrors)
|
||||
return mirrors, count, err
|
||||
return db.FindAndCount[PushMirror](ctx, findPushMirrorOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: repoID,
|
||||
})
|
||||
}
|
||||
|
||||
func GetPushMirrorByIDAndRepoID(ctx context.Context, id, repoID int64) (*PushMirror, bool, error) {
|
||||
var pushMirror PushMirror
|
||||
has, err := db.GetEngine(ctx).Where("id = ?", id).And("repo_id = ?", repoID).Get(&pushMirror)
|
||||
if !has || err != nil {
|
||||
return nil, has, err
|
||||
}
|
||||
mirrors := make([]*PushMirror, 0, 10)
|
||||
count, err := sess.FindAndCount(&mirrors)
|
||||
return mirrors, count, err
|
||||
return &pushMirror, true, nil
|
||||
}
|
||||
|
||||
// GetPushMirrorsSyncedOnCommit returns push-mirrors for this repo that should be updated by new commits
|
||||
func GetPushMirrorsSyncedOnCommit(ctx context.Context, repoID int64) ([]*PushMirror, error) {
|
||||
mirrors := make([]*PushMirror, 0, 10)
|
||||
return mirrors, db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND sync_on_commit = ?", repoID, true).
|
||||
Find(&mirrors)
|
||||
return db.Find[PushMirror](ctx, findPushMirrorOptions{
|
||||
RepoID: repoID,
|
||||
SyncOnCommit: optional.Some(true),
|
||||
})
|
||||
}
|
||||
|
||||
// PushMirrorsIterate iterates all push-mirror repositories.
|
||||
|
@ -617,10 +617,7 @@ func (repo *Repository) CanEnableEditor() bool {
|
||||
|
||||
// DescriptionHTML does special handles to description and return HTML string.
|
||||
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
|
||||
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
// Don't use Metas to speedup requests
|
||||
}, repo.Description)
|
||||
desc, err := markup.RenderDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
|
||||
if err != nil {
|
||||
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
|
||||
return template.HTML(markup.SanitizeDescription(repo.Description))
|
||||
|
@ -98,8 +98,7 @@ func (repos RepositoryList) IDs() []int64 {
|
||||
return repoIDs
|
||||
}
|
||||
|
||||
// LoadAttributes loads the attributes for the given RepositoryList
|
||||
func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
||||
func (repos RepositoryList) LoadOwners(ctx context.Context) error {
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -107,10 +106,6 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
||||
userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) {
|
||||
return repo.OwnerID, true
|
||||
})
|
||||
repoIDs := make([]int64, len(repos))
|
||||
for i := range repos {
|
||||
repoIDs[i] = repos[i].ID
|
||||
}
|
||||
|
||||
// Load owners.
|
||||
users := make(map[int64]*user_model.User, len(userIDs))
|
||||
@ -123,12 +118,19 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
||||
for i := range repos {
|
||||
repos[i].Owner = users[repos[i].OwnerID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repos RepositoryList) LoadLanguageStats(ctx context.Context) error {
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load primary language.
|
||||
stats := make(LanguageStatList, 0, len(repos))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("`is_primary` = ? AND `language` != ?", true, "other").
|
||||
In("`repo_id`", repoIDs).
|
||||
In("`repo_id`", repos.IDs()).
|
||||
Find(&stats); err != nil {
|
||||
return fmt.Errorf("find primary languages: %w", err)
|
||||
}
|
||||
@ -141,10 +143,18 @@ func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads the attributes for the given RepositoryList
|
||||
func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
|
||||
if err := repos.LoadOwners(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repos.LoadLanguageStats(ctx)
|
||||
}
|
||||
|
||||
// SearchRepoOptions holds the search options
|
||||
type SearchRepoOptions struct {
|
||||
db.ListOptions
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@ -146,57 +145,6 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetReviewers get all users can be requested to review:
|
||||
// * for private repositories this returns all users that have read access or higher to the repository.
|
||||
// * for public repositories this returns all users that have read access or higher to the repository,
|
||||
// all repo watchers and all organization members.
|
||||
// TODO: may be we should have a busy choice for users to block review request to them.
|
||||
func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) {
|
||||
// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := builder.And(builder.Neq{"`user`.id": posterID}).
|
||||
And(builder.Eq{"`user`.is_active": true})
|
||||
|
||||
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
|
||||
// This a private repository:
|
||||
// Anyone who can read the repository is a requestable reviewer
|
||||
|
||||
cond = cond.And(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("access").Where(
|
||||
builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.Gte{"mode": perm.AccessModeRead}),
|
||||
),
|
||||
))
|
||||
|
||||
if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID {
|
||||
// as private *user* repos don't generate an entry in the `access` table,
|
||||
// the owner of a private repo needs to be explicitly added.
|
||||
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID})
|
||||
}
|
||||
} else {
|
||||
// This is a "public" repository:
|
||||
// Any user that has read access, is a watcher or organization member can be requested to review
|
||||
cond = cond.And(builder.And(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("access").
|
||||
Where(builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.Gte{"mode": perm.AccessModeRead})),
|
||||
).Or(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("watch").
|
||||
Where(builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.In("mode", WatchModeNormal, WatchModeAuto))),
|
||||
).Or(builder.In("`user`.id",
|
||||
builder.Select("uid").From("org_user").
|
||||
Where(builder.Eq{"org_id": repo.OwnerID}),
|
||||
)))))
|
||||
}
|
||||
|
||||
users := make([]*user_model.User, 0, 8)
|
||||
return users, db.GetEngine(ctx).Where(cond).OrderBy(user_model.GetOrderByName()).Find(&users)
|
||||
}
|
||||
|
||||
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
|
||||
// If isShowFullName is set to true, also include full name prefix search
|
||||
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
|
||||
|
@ -38,46 +38,3 @@ func TestRepoAssignees(t *testing.T) {
|
||||
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoGetReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// test public repo
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
ctx := db.DefaultContext
|
||||
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, reviewers, 3) {
|
||||
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
|
||||
}
|
||||
|
||||
// should include doer if doer is not PR poster.
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 3)
|
||||
|
||||
// should not include PR poster, if PR poster would be otherwise eligible
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
// test private user repo
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
assert.EqualValues(t, reviewers[0].ID, 2)
|
||||
|
||||
// test private org repo
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
}
|
||||
|
@ -48,19 +48,19 @@ const (
|
||||
UserTypeIndividual UserType = iota // Historic reason to make it starts at 0.
|
||||
|
||||
// UserTypeOrganization defines an organization
|
||||
UserTypeOrganization
|
||||
UserTypeOrganization // 1
|
||||
|
||||
// UserTypeUserReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on
|
||||
UserTypeUserReserved
|
||||
UserTypeUserReserved // 2
|
||||
|
||||
// UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved
|
||||
UserTypeOrganizationReserved
|
||||
UserTypeOrganizationReserved // 3
|
||||
|
||||
// UserTypeBot defines a bot user
|
||||
UserTypeBot
|
||||
UserTypeBot // 4
|
||||
|
||||
// UserTypeRemoteUser defines a remote user for federated users
|
||||
UserTypeRemoteUser
|
||||
UserTypeRemoteUser // 5
|
||||
)
|
||||
|
||||
const (
|
||||
@ -884,7 +884,13 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
|
||||
|
||||
// GetInactiveUsers gets all inactive users
|
||||
func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, error) {
|
||||
var cond builder.Cond = builder.Eq{"is_active": false}
|
||||
cond := builder.And(
|
||||
builder.Eq{"is_active": false},
|
||||
builder.Or( // only plain user
|
||||
builder.Eq{"`type`": UserTypeIndividual},
|
||||
builder.Eq{"`type`": UserTypeUserReserved},
|
||||
),
|
||||
)
|
||||
|
||||
if olderThan > 0 {
|
||||
cond = cond.And(builder.Lt{"created_unix": time.Now().Add(-olderThan).Unix()})
|
||||
|
@ -588,3 +588,17 @@ func TestDisabledUserFeatures(t *testing.T) {
|
||||
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInactiveUsers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// all inactive users
|
||||
// user1's createdunix is 1730468968
|
||||
users, err := user_model.GetInactiveUsers(db.DefaultContext, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 1)
|
||||
interval := time.Now().Unix() - 1730468968 + 3600*24
|
||||
users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second)))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 0)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
stdcsv "encoding/csv"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -53,7 +53,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
|
||||
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
|
||||
extension := ".csv"
|
||||
if ctx != nil {
|
||||
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||
extension = strings.ToLower(path.Ext(ctx.RenderOptions.RelativePath))
|
||||
}
|
||||
|
||||
var delimiter rune
|
||||
|
@ -5,13 +5,13 @@ package csv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
@ -231,10 +231,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimiters`,
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
delimiter := determineDelimiter(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: c.filename,
|
||||
}, []byte(decodeSlashes(t, c.csv)))
|
||||
delimiter := determineDelimiter(markup.NewRenderContext(context.Background()).WithRelativePath(c.filename), []byte(decodeSlashes(t, c.csv)))
|
||||
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
@ -29,7 +28,7 @@ type Commit struct {
|
||||
Signature *CommitSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
submoduleCache *ObjectCache[*SubModule]
|
||||
}
|
||||
|
||||
// CommitSignature represents a git commit signature part.
|
||||
@ -357,69 +356,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) {
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// GetSubModules get all the sub modules of current revision git tree
|
||||
func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
||||
if c.submoduleCache != nil {
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rd.Close()
|
||||
scanner := bufio.NewScanner(rd)
|
||||
c.submoduleCache = newObjectCache()
|
||||
var ismodule bool
|
||||
var path string
|
||||
for scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "[submodule") {
|
||||
ismodule = true
|
||||
continue
|
||||
}
|
||||
if ismodule {
|
||||
fields := strings.Split(scanner.Text(), "=")
|
||||
k := strings.TrimSpace(fields[0])
|
||||
if k == "path" {
|
||||
path = strings.TrimSpace(fields[1])
|
||||
} else if k == "url" {
|
||||
c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])})
|
||||
ismodule = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("GetSubModules scan: %w", err)
|
||||
}
|
||||
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
// GetSubModule get the sub module according entryname
|
||||
func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
|
||||
modules, err := c.GetSubModules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if modules != nil {
|
||||
module, has := modules.Get(entryname)
|
||||
if has {
|
||||
return module.(*SubModule), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
|
||||
func (c *Commit) GetBranchName() (string, error) {
|
||||
cmd := NewCommand(c.repo.Ctx, "name-rev")
|
||||
|
@ -7,5 +7,5 @@ package git
|
||||
type CommitInfo struct {
|
||||
Entry *TreeEntry
|
||||
Commit *Commit
|
||||
SubModuleFile *SubModuleFile
|
||||
SubModuleFile *CommitSubModuleFile
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
commitsInfo[i].Commit = entryCommit
|
||||
}
|
||||
|
||||
// If the entry if a submodule add a submodule file for this
|
||||
// If the entry is a submodule add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
subModuleURL := ""
|
||||
var fullPath string
|
||||
@ -85,7 +85,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
} else if subModule != nil {
|
||||
subModuleURL = subModule.URL
|
||||
}
|
||||
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
||||
subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String())
|
||||
commitsInfo[i].SubModuleFile = subModuleFile
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
} else if subModule != nil {
|
||||
subModuleURL = subModule.URL
|
||||
}
|
||||
subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String())
|
||||
subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String())
|
||||
commitsInfo[i].SubModuleFile = subModuleFile
|
||||
}
|
||||
}
|
||||
|
47
modules/git/commit_submodule.go
Normal file
47
modules/git/commit_submodule.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
// GetSubModules get all the submodules of current revision git tree
|
||||
func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
||||
if c.submoduleCache != nil {
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd, err := entry.Blob().DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rd.Close()
|
||||
|
||||
// at the moment we do not strictly limit the size of the .gitmodules file because some users would have huge .gitmodules files (>1MB)
|
||||
c.submoduleCache, err = configParseSubModules(rd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
// GetSubModule get the submodule according entry name
|
||||
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
|
||||
modules, err := c.GetSubModules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if modules != nil {
|
||||
if module, has := modules.Get(entryName); has {
|
||||
return module, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
@ -15,24 +15,15 @@ import (
|
||||
|
||||
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
|
||||
|
||||
// SubModule submodule is a reference on git repository
|
||||
type SubModule struct {
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
// SubModuleFile represents a file with submodule type.
|
||||
type SubModuleFile struct {
|
||||
*Commit
|
||||
|
||||
// CommitSubModuleFile represents a file with submodule type.
|
||||
type CommitSubModuleFile struct {
|
||||
refURL string
|
||||
refID string
|
||||
}
|
||||
|
||||
// NewSubModuleFile create a new submodule file
|
||||
func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile {
|
||||
return &SubModuleFile{
|
||||
Commit: c,
|
||||
// NewCommitSubModuleFile create a new submodule file
|
||||
func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile {
|
||||
return &CommitSubModuleFile{
|
||||
refURL: refURL,
|
||||
refID: refID,
|
||||
}
|
||||
@ -109,11 +100,12 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string {
|
||||
}
|
||||
|
||||
// RefURL guesses and returns reference URL.
|
||||
func (sf *SubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string {
|
||||
// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore)
|
||||
func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string {
|
||||
return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain)
|
||||
}
|
||||
|
||||
// RefID returns reference ID.
|
||||
func (sf *SubModuleFile) RefID() string {
|
||||
func (sf *CommitSubModuleFile) RefID() string {
|
||||
return sf.refID
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRefURL(t *testing.T) {
|
||||
func TestCommitSubModuleFileGetRefURL(t *testing.T) {
|
||||
kases := []struct {
|
||||
refURL string
|
||||
prefixURL string
|
@ -135,7 +135,7 @@ author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||
encoding ISO-8859-1
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
<SPACE>
|
||||
iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow
|
||||
Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR
|
||||
gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq
|
||||
@ -150,7 +150,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
ISO-8859-1`
|
||||
|
||||
commitString = strings.ReplaceAll(commitString, "<SPACE>", " ")
|
||||
sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
|
||||
assert.NoError(t, err)
|
||||
|
187
modules/git/config.go
Normal file
187
modules/git/config.go
Normal file
@ -0,0 +1,187 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem)
|
||||
func syncGitConfig() (err error) {
|
||||
if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err)
|
||||
}
|
||||
|
||||
// first, write user's git config options to git config file
|
||||
// user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes
|
||||
for k, v := range setting.GitConfig.Options {
|
||||
if err = configSet(strings.ToLower(k), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
|
||||
// TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used.
|
||||
// If these values are not really used, then they can be set (overwritten) directly without considering about existence.
|
||||
for configKey, defaultValue := range map[string]string{
|
||||
"user.name": "Gitea",
|
||||
"user.email": "gitea@fake.local",
|
||||
} {
|
||||
if err := configSetNonExist(configKey, defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set git some configurations - these must be set to these values for gitea to work correctly
|
||||
if err := configSet("core.quotePath", "false"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.10") {
|
||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.18") {
|
||||
if err := configSet("core.commitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configSet("gc.writeCommitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configSet("fetch.writeCommitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultFeatures().SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
|
||||
// However, some docker users and samba users find it difficult to configure their systems correctly,
|
||||
// so that Gitea's git repositories are owned by the Gitea user.
|
||||
// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
|
||||
// See issue: https://github.com/go-gitea/gitea/issues/19455
|
||||
// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
|
||||
// it is now safe to set "safe.directory=*" for internal usage only.
|
||||
// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
|
||||
if err := configAddNonExist("safe.directory", "*"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := configSet("core.longpaths", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if setting.Git.DisableCoreProtectNTFS {
|
||||
err = configSet("core.protectNTFS", "false")
|
||||
} else {
|
||||
err = configUnsetAll("core.protectNTFS", "false")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// By default partial clones are disabled, enable them from git v2.22
|
||||
if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") {
|
||||
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = configSet("uploadpack.allowAnySHA1InWant", "true")
|
||||
} else {
|
||||
if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func configSet(key, value string) error {
|
||||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err != nil && !IsErrorExitCode(err, 1) {
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
currValue := strings.TrimSpace(stdout)
|
||||
if currValue == value {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSetNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, set new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
func configAddNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, add new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
func configUnsetAll(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// exist, need to remove
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
75
modules/git/config_submodule.go
Normal file
75
modules/git/config_submodule.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SubModule is a reference on git repository
|
||||
type SubModule struct {
|
||||
Path string
|
||||
URL string
|
||||
Branch string // this field is newly added but not really used
|
||||
}
|
||||
|
||||
// configParseSubModules this is not a complete parse for gitmodules file, it only
|
||||
// parses the url and path of submodules. At the moment it only parses well-formed gitmodules files.
|
||||
// In the future, there should be a complete implementation of https://git-scm.com/docs/git-config#_syntax
|
||||
func configParseSubModules(r io.Reader) (*ObjectCache[*SubModule], error) {
|
||||
var subModule *SubModule
|
||||
subModules := newObjectCache[*SubModule]()
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Section header [section]
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if subModule != nil {
|
||||
subModules.Set(subModule.Path, subModule)
|
||||
}
|
||||
if strings.HasPrefix(line, "[submodule") {
|
||||
subModule = &SubModule{}
|
||||
} else {
|
||||
subModule = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if subModule == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
switch key {
|
||||
case "path":
|
||||
subModule.Path = value
|
||||
case "url":
|
||||
subModule.URL = value
|
||||
case "branch":
|
||||
subModule.Branch = value
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
if subModule != nil {
|
||||
subModules.Set(subModule.Path, subModule)
|
||||
}
|
||||
return subModules, nil
|
||||
}
|
49
modules/git/config_submodule_test.go
Normal file
49
modules/git/config_submodule_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigSubmodule(t *testing.T) {
|
||||
input := `
|
||||
[core]
|
||||
path = test
|
||||
|
||||
[submodule "submodule1"]
|
||||
path = path1
|
||||
url = https://gitea.io/foo/foo
|
||||
#branch = b1
|
||||
|
||||
[other1]
|
||||
branch = master
|
||||
|
||||
[submodule "submodule2"]
|
||||
path = path2
|
||||
url = https://gitea.io/bar/bar
|
||||
branch = b2
|
||||
|
||||
[other2]
|
||||
branch = main
|
||||
|
||||
[submodule "submodule3"]
|
||||
path = path3
|
||||
url = https://gitea.io/xxx/xxx
|
||||
`
|
||||
|
||||
subModules, err := configParseSubModules(strings.NewReader(input))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, subModules.cache, 3)
|
||||
|
||||
sm1, _ := subModules.Get("path1")
|
||||
assert.Equal(t, &SubModule{Path: "path1", URL: "https://gitea.io/foo/foo", Branch: ""}, sm1)
|
||||
sm2, _ := subModules.Get("path2")
|
||||
assert.Equal(t, &SubModule{Path: "path2", URL: "https://gitea.io/bar/bar", Branch: "b2"}, sm2)
|
||||
sm3, _ := subModules.Get("path3")
|
||||
assert.Equal(t, &SubModule{Path: "path3", URL: "https://gitea.io/xxx/xxx", Branch: ""}, sm3)
|
||||
}
|
66
modules/git/config_test.go
Normal file
66
modules/git/config_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func gitConfigContains(sub string) bool {
|
||||
if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil {
|
||||
return strings.Contains(string(b), sub)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestGitConfig(t *testing.T) {
|
||||
assert.False(t, gitConfigContains("key-a"))
|
||||
|
||||
assert.NoError(t, configSetNonExist("test.key-a", "val-a"))
|
||||
assert.True(t, gitConfigContains("key-a = val-a"))
|
||||
|
||||
assert.NoError(t, configSetNonExist("test.key-a", "val-a-changed"))
|
||||
assert.False(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configSet("test.key-a", "val-a-changed"))
|
||||
assert.True(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configAddNonExist("test.key-b", "val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||
|
||||
assert.NoError(t, configAddNonExist("test.key-b", "val-2b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configUnsetAll("test.key-b", "val-b"))
|
||||
assert.False(t, gitConfigContains("key-b = val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configUnsetAll("test.key-b", "val-2b"))
|
||||
assert.False(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configSet("test.key-x", "*"))
|
||||
assert.True(t, gitConfigContains("key-x = *"))
|
||||
assert.NoError(t, configSetNonExist("test.key-x", "*"))
|
||||
assert.NoError(t, configUnsetAll("test.key-x", "*"))
|
||||
assert.False(t, gitConfigContains("key-x = *"))
|
||||
}
|
||||
|
||||
func TestSyncConfig(t *testing.T) {
|
||||
oldGitConfig := setting.GitConfig
|
||||
defer func() {
|
||||
setting.GitConfig = oldGitConfig
|
||||
}()
|
||||
|
||||
setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA"
|
||||
assert.NoError(t, syncGitConfig())
|
||||
assert.True(t, gitConfigContains("[sync-test]"))
|
||||
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
|
||||
}
|
14
modules/git/fsck.go
Normal file
14
modules/git/fsck.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fsck verifies the connectivity and validity of the objects in the database
|
||||
func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error {
|
||||
return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath})
|
||||
}
|
@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -95,17 +94,18 @@ func parseGitVersionLine(s string) (*version.Version, error) {
|
||||
return version.NewVersion(versionString)
|
||||
}
|
||||
|
||||
// SetExecutablePath changes the path of git executable and checks the file permission and version.
|
||||
func SetExecutablePath(path string) error {
|
||||
// If path is empty, we use the default value of GitExecutable "git" to search for the location of git.
|
||||
if path != "" {
|
||||
GitExecutable = path
|
||||
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||
badVersions := []struct {
|
||||
Version *version.Version
|
||||
Reason string
|
||||
}{
|
||||
{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
|
||||
}
|
||||
absPath, err := exec.LookPath(GitExecutable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("git not found: %w", err)
|
||||
for _, bad := range badVersions {
|
||||
if gitVer.Equal(bad.Version) {
|
||||
return errors.New(bad.Reason)
|
||||
}
|
||||
}
|
||||
GitExecutable = absPath
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -128,6 +128,20 @@ func ensureGitVersion() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExecutablePath changes the path of git executable and checks the file permission and version.
|
||||
func SetExecutablePath(path string) error {
|
||||
// If path is empty, we use the default value of GitExecutable "git" to search for the location of git.
|
||||
if path != "" {
|
||||
GitExecutable = path
|
||||
}
|
||||
absPath, err := exec.LookPath(GitExecutable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("git not found: %w", err)
|
||||
}
|
||||
GitExecutable = absPath
|
||||
return nil
|
||||
}
|
||||
|
||||
// HomeDir is the home dir for git to store the global config file used by Gitea internally
|
||||
func HomeDir() string {
|
||||
if setting.Git.HomePath == "" {
|
||||
@ -204,196 +218,3 @@ func InitFull(ctx context.Context) (err error) {
|
||||
|
||||
return syncGitConfig()
|
||||
}
|
||||
|
||||
// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem)
|
||||
func syncGitConfig() (err error) {
|
||||
if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err)
|
||||
}
|
||||
|
||||
// first, write user's git config options to git config file
|
||||
// user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes
|
||||
for k, v := range setting.GitConfig.Options {
|
||||
if err = configSet(strings.ToLower(k), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
|
||||
// TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used.
|
||||
// If these values are not really used, then they can be set (overwritten) directly without considering about existence.
|
||||
for configKey, defaultValue := range map[string]string{
|
||||
"user.name": "Gitea",
|
||||
"user.email": "gitea@fake.local",
|
||||
} {
|
||||
if err := configSetNonExist(configKey, defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set git some configurations - these must be set to these values for gitea to work correctly
|
||||
if err := configSet("core.quotePath", "false"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.10") {
|
||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultFeatures().CheckVersionAtLeast("2.18") {
|
||||
if err := configSet("core.commitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configSet("gc.writeCommitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configSet("fetch.writeCommitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultFeatures().SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
|
||||
// However, some docker users and samba users find it difficult to configure their systems correctly,
|
||||
// so that Gitea's git repositories are owned by the Gitea user.
|
||||
// (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
|
||||
// See issue: https://github.com/go-gitea/gitea/issues/19455
|
||||
// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
|
||||
// it is now safe to set "safe.directory=*" for internal usage only.
|
||||
// Although this setting is only supported by some new git versions, it is also tolerated by earlier versions
|
||||
if err := configAddNonExist("safe.directory", "*"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := configSet("core.longpaths", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
if setting.Git.DisableCoreProtectNTFS {
|
||||
err = configSet("core.protectNTFS", "false")
|
||||
} else {
|
||||
err = configUnsetAll("core.protectNTFS", "false")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// By default partial clones are disabled, enable them from git v2.22
|
||||
if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") {
|
||||
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = configSet("uploadpack.allowAnySHA1InWant", "true")
|
||||
} else {
|
||||
if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||
badVersions := []struct {
|
||||
Version *version.Version
|
||||
Reason string
|
||||
}{
|
||||
{version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"},
|
||||
}
|
||||
for _, bad := range badVersions {
|
||||
if gitVer.Equal(bad.Version) {
|
||||
return errors.New(bad.Reason)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSet(key, value string) error {
|
||||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err != nil && !IsErrorExitCode(err, 1) {
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
currValue := strings.TrimSpace(stdout)
|
||||
if currValue == value {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configSetNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, set new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
func configAddNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, add new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
func configUnsetAll(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// exist, need to remove
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
|
||||
// Fsck verifies the connectivity and validity of the objects in the database
|
||||
func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error {
|
||||
return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath})
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -43,58 +42,6 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
}
|
||||
|
||||
func gitConfigContains(sub string) bool {
|
||||
if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil {
|
||||
return strings.Contains(string(b), sub)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestGitConfig(t *testing.T) {
|
||||
assert.False(t, gitConfigContains("key-a"))
|
||||
|
||||
assert.NoError(t, configSetNonExist("test.key-a", "val-a"))
|
||||
assert.True(t, gitConfigContains("key-a = val-a"))
|
||||
|
||||
assert.NoError(t, configSetNonExist("test.key-a", "val-a-changed"))
|
||||
assert.False(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configSet("test.key-a", "val-a-changed"))
|
||||
assert.True(t, gitConfigContains("key-a = val-a-changed"))
|
||||
|
||||
assert.NoError(t, configAddNonExist("test.key-b", "val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||
|
||||
assert.NoError(t, configAddNonExist("test.key-b", "val-2b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configUnsetAll("test.key-b", "val-b"))
|
||||
assert.False(t, gitConfigContains("key-b = val-b"))
|
||||
assert.True(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configUnsetAll("test.key-b", "val-2b"))
|
||||
assert.False(t, gitConfigContains("key-b = val-2b"))
|
||||
|
||||
assert.NoError(t, configSet("test.key-x", "*"))
|
||||
assert.True(t, gitConfigContains("key-x = *"))
|
||||
assert.NoError(t, configSetNonExist("test.key-x", "*"))
|
||||
assert.NoError(t, configUnsetAll("test.key-x", "*"))
|
||||
assert.False(t, gitConfigContains("key-x = *"))
|
||||
}
|
||||
|
||||
func TestSyncConfig(t *testing.T) {
|
||||
oldGitConfig := setting.GitConfig
|
||||
defer func() {
|
||||
setting.GitConfig = oldGitConfig
|
||||
}()
|
||||
|
||||
setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA"
|
||||
assert.NoError(t, syncGitConfig())
|
||||
assert.True(t, gitConfigContains("[sync-test]"))
|
||||
assert.True(t, gitConfigContains("cfg-key-a = CfgValA"))
|
||||
}
|
||||
|
||||
func TestParseGitVersion(t *testing.T) {
|
||||
v, err := parseGitVersionLine("git version 2.29.3")
|
||||
assert.NoError(t, err)
|
||||
|
@ -28,7 +28,7 @@ const isGogit = true
|
||||
type Repository struct {
|
||||
Path string
|
||||
|
||||
tagCache *ObjectCache
|
||||
tagCache *ObjectCache[*Tag]
|
||||
|
||||
gogitRepo *gogit.Repository
|
||||
gogitStorage *filesystem.Storage
|
||||
@ -79,7 +79,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
||||
Path: repoPath,
|
||||
gogitRepo: gogitRepo,
|
||||
gogitStorage: storage,
|
||||
tagCache: newObjectCache(),
|
||||
tagCache: newObjectCache[*Tag](),
|
||||
Ctx: ctx,
|
||||
objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(),
|
||||
}, nil
|
||||
|
@ -21,7 +21,7 @@ const isGogit = false
|
||||
type Repository struct {
|
||||
Path string
|
||||
|
||||
tagCache *ObjectCache
|
||||
tagCache *ObjectCache[*Tag]
|
||||
|
||||
gpgSettings *GPGSettings
|
||||
|
||||
@ -53,7 +53,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
||||
|
||||
return &Repository{
|
||||
Path: repoPath,
|
||||
tagCache: newObjectCache(),
|
||||
tagCache: newObjectCache[*Tag](),
|
||||
Ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||
t, ok := repo.tagCache.Get(tagID.String())
|
||||
if ok {
|
||||
log.Debug("Hit cache: %s", tagID)
|
||||
tagClone := *t.(*Tag)
|
||||
tagClone := *t
|
||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
||||
return &tagClone, nil
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) {
|
||||
t, ok := repo.tagCache.Get(tagID.String())
|
||||
if ok {
|
||||
log.Debug("Hit cache: %s", tagID)
|
||||
tagClone := *t.(*Tag)
|
||||
tagClone := *t
|
||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
||||
return &tagClone, nil
|
||||
}
|
||||
|
@ -15,27 +15,25 @@ import (
|
||||
)
|
||||
|
||||
// ObjectCache provides thread-safe cache operations.
|
||||
type ObjectCache struct {
|
||||
type ObjectCache[T any] struct {
|
||||
lock sync.RWMutex
|
||||
cache map[string]any
|
||||
cache map[string]T
|
||||
}
|
||||
|
||||
func newObjectCache() *ObjectCache {
|
||||
return &ObjectCache{
|
||||
cache: make(map[string]any, 10),
|
||||
}
|
||||
func newObjectCache[T any]() *ObjectCache[T] {
|
||||
return &ObjectCache[T]{cache: make(map[string]T, 10)}
|
||||
}
|
||||
|
||||
// Set add obj to cache
|
||||
func (oc *ObjectCache) Set(id string, obj any) {
|
||||
// Set adds obj to cache
|
||||
func (oc *ObjectCache[T]) Set(id string, obj T) {
|
||||
oc.lock.Lock()
|
||||
defer oc.lock.Unlock()
|
||||
|
||||
oc.cache[id] = obj
|
||||
}
|
||||
|
||||
// Get get cached obj by id
|
||||
func (oc *ObjectCache) Get(id string) (any, bool) {
|
||||
// Get gets cached obj by id
|
||||
func (oc *ObjectCache[T]) Get(id string) (T, bool) {
|
||||
oc.lock.RLock()
|
||||
defer oc.lock.RUnlock()
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package html
|
||||
|
||||
// ParseSizeAndClass get size and class from string with default values
|
||||
// If present, "others" expects the new size first and then the classes to use
|
||||
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
|
||||
size := defaultSize
|
||||
if len(others) >= 1 {
|
||||
if v, ok := others[0].(int); ok && v != 0 {
|
||||
size = v
|
||||
}
|
||||
}
|
||||
class := defaultClass
|
||||
if len(others) >= 2 {
|
||||
if v, ok := others[1].(string); ok && v != "" {
|
||||
if class != "" {
|
||||
class += " "
|
||||
}
|
||||
class += v
|
||||
}
|
||||
}
|
||||
return size, class
|
||||
}
|
48
modules/htmlutil/html.go
Normal file
48
modules/htmlutil/html.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// ParseSizeAndClass get size and class from string with default values
|
||||
// If present, "others" expects the new size first and then the classes to use
|
||||
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
|
||||
size := defaultSize
|
||||
if len(others) >= 1 {
|
||||
if v, ok := others[0].(int); ok && v != 0 {
|
||||
size = v
|
||||
}
|
||||
}
|
||||
class := defaultClass
|
||||
if len(others) >= 2 {
|
||||
if v, ok := others[1].(string); ok && v != "" {
|
||||
if class != "" {
|
||||
class += " "
|
||||
}
|
||||
class += v
|
||||
}
|
||||
}
|
||||
return size, class
|
||||
}
|
||||
|
||||
func HTMLFormat(s string, rawArgs ...any) template.HTML {
|
||||
args := slices.Clone(rawArgs)
|
||||
for i, v := range args {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
||||
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
||||
case string:
|
||||
args[i] = template.HTMLEscapeString(v)
|
||||
case fmt.Stringer:
|
||||
args[i] = template.HTMLEscapeString(v.String())
|
||||
default:
|
||||
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
return template.HTML(fmt.Sprintf(s, args...))
|
||||
}
|
15
modules/htmlutil/html_test.go
Normal file
15
modules/htmlutil/html_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package htmlutil
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHTMLFormat(t *testing.T) {
|
||||
assert.Equal(t, template.HTML("<a>< < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
|
||||
}
|
@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -38,27 +37,17 @@ const (
|
||||
|
||||
// SanitizerRules implements markup.Renderer
|
||||
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return []setting.MarkupSanitizerRule{
|
||||
{Element: "div", AllowAttr: "class", Regexp: regexp.MustCompile(playerClassName)},
|
||||
{Element: "div", AllowAttr: playerSrcAttr},
|
||||
}
|
||||
return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}}
|
||||
}
|
||||
|
||||
// Render implements markup.Renderer
|
||||
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
|
||||
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
|
||||
setting.AppSubURL,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
ctx.RenderOptions.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RenderOptions.RelativePath),
|
||||
)
|
||||
|
||||
_, err := io.WriteString(output, fmt.Sprintf(
|
||||
`<div class="%s" %s="%s"></div>`,
|
||||
playerClassName,
|
||||
playerSrcAttr,
|
||||
rawURL,
|
||||
))
|
||||
return err
|
||||
return ctx.RenderInternal.FormatWithSafeAttrs(output, `<div class="%s" %s="%s"></div>`, playerClassName, playerSrcAttr, rawURL)
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
// NOTE: All below regex matching do not perform any extra validation.
|
||||
// Thus a link is produced even if the linked entity does not exist.
|
||||
// While fast, this is also incorrect and lead to false positives.
|
||||
// TODO: fix invalid linking issue
|
||||
|
||||
// LinkRegex is a regexp matching a valid link
|
||||
var LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
|
@ -9,15 +9,27 @@ package common
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
|
||||
type GlobalVarsType struct {
|
||||
wwwURLRegxp *regexp.Regexp
|
||||
LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation.
|
||||
}
|
||||
|
||||
var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType {
|
||||
v := &GlobalVarsType{}
|
||||
v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
|
||||
v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
|
||||
return v
|
||||
})
|
||||
|
||||
type linkifyParser struct{}
|
||||
|
||||
@ -60,10 +72,10 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
|
||||
var protocol []byte
|
||||
typ := ast.AutoLinkURL
|
||||
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
|
||||
m = LinkRegex.FindSubmatchIndex(line)
|
||||
m = GlobalVars().LinkRegex.FindSubmatchIndex(line)
|
||||
}
|
||||
if m == nil && bytes.HasPrefix(line, domainWWW) {
|
||||
m = wwwURLRegxp.FindSubmatchIndex(line)
|
||||
m = GlobalVars().wwwURLRegxp.FindSubmatchIndex(line)
|
||||
protocol = []byte("http")
|
||||
}
|
||||
if m != nil {
|
||||
|
@ -6,8 +6,7 @@ package console
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -36,7 +35,7 @@ func (Renderer) Extensions() []string {
|
||||
// SanitizerRules implements markup.Renderer
|
||||
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return []setting.MarkupSanitizerRule{
|
||||
{Element: "span", AllowAttr: "class", Regexp: regexp.MustCompile(`^term-((fg[ix]?|bg)\d+|container)$`)},
|
||||
{Element: "span", AllowAttr: "class", Regexp: `^term-((fg[ix]?|bg)\d+|container)$`},
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +45,7 @@ func (Renderer) CanRender(filename string, input io.Reader) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if enry.GetLanguage(filepath.Base(filename), buf) != enry.OtherLanguage {
|
||||
if enry.GetLanguage(path.Base(filename), buf) != enry.OtherLanguage {
|
||||
return false
|
||||
}
|
||||
return bytes.ContainsRune(buf, '\x1b')
|
||||
|
@ -4,10 +4,10 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -24,8 +24,7 @@ func TestRenderConsole(t *testing.T) {
|
||||
canRender := render.CanRender("test", strings.NewReader(k))
|
||||
assert.True(t, canRender)
|
||||
|
||||
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
|
||||
strings.NewReader(k), &buf)
|
||||
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, v, buf.String())
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"html"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/csv"
|
||||
@ -37,9 +36,9 @@ func (Renderer) Extensions() []string {
|
||||
// SanitizerRules implements markup.Renderer
|
||||
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return []setting.MarkupSanitizerRule{
|
||||
{Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)},
|
||||
{Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
|
||||
{Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
|
||||
{Element: "table", AllowAttr: "class", Regexp: `^data-table$`},
|
||||
{Element: "th", AllowAttr: "class", Regexp: `^line-num$`},
|
||||
{Element: "td", AllowAttr: "class", Regexp: `^line-num$`},
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,13 +50,13 @@ func writeField(w io.Writer, element, class, field string) error {
|
||||
return err
|
||||
}
|
||||
if len(class) > 0 {
|
||||
if _, err := io.WriteString(w, " class=\""); err != nil {
|
||||
if _, err := io.WriteString(w, ` class="`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w, class); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.WriteString(w, "\""); err != nil {
|
||||
if _, err := io.WriteString(w, `"`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -134,10 +133,10 @@ func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.W
|
||||
// Check if maxRows or maxSize is reached, and if true, warn.
|
||||
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
|
||||
warn := `<table class="data-table"><tr><td>`
|
||||
rawLink := ` <a href="` + ctx.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RelativePath) + `">`
|
||||
rawLink := ` <a href="` + ctx.RenderOptions.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RenderOptions.RelativePath) + `">`
|
||||
|
||||
// Try to get the user translation
|
||||
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
warn += locale.TrString("repo.file_too_large")
|
||||
rawLink += locale.TrString("repo.file_view_raw")
|
||||
} else {
|
||||
|
@ -4,10 +4,10 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -24,8 +24,7 @@ func TestRenderCSV(t *testing.T) {
|
||||
|
||||
for k, v := range kases {
|
||||
var buf strings.Builder
|
||||
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
|
||||
strings.NewReader(k), &buf)
|
||||
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, v, buf.String())
|
||||
}
|
||||
|
22
modules/markup/external/external.go
vendored
22
modules/markup/external/external.go
vendored
@ -12,7 +12,6 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
@ -80,8 +79,8 @@ func envMark(envName string) string {
|
||||
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
var (
|
||||
command = strings.NewReplacer(
|
||||
envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
|
||||
envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
|
||||
envMark("GITEA_PREFIX_SRC"), ctx.RenderOptions.Links.SrcLink(),
|
||||
envMark("GITEA_PREFIX_RAW"), ctx.RenderOptions.Links.RawLink(),
|
||||
).Replace(p.Command)
|
||||
commands = strings.Fields(command)
|
||||
args = commands[1:]
|
||||
@ -102,7 +101,7 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
|
||||
_, err = io.Copy(f, input)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
return fmt.Errorf("%s write data to temp file when rendering %s failed: %w", p.Name(), p.Command, err)
|
||||
}
|
||||
|
||||
@ -113,23 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
args = append(args, f.Name())
|
||||
}
|
||||
|
||||
if ctx == nil || ctx.Ctx == nil {
|
||||
if ctx == nil {
|
||||
log.Warn("RenderContext not provided defaulting to empty ctx")
|
||||
ctx = &markup.RenderContext{}
|
||||
}
|
||||
log.Warn("RenderContext did not provide context, defaulting to Shutdown context")
|
||||
ctx.Ctx = graceful.GetManager().ShutdownContext()
|
||||
}
|
||||
|
||||
processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
|
||||
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderOptions.Links.SrcLink()))
|
||||
defer finished()
|
||||
|
||||
cmd := exec.CommandContext(processCtx, commands[0], args...)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
|
||||
"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
|
||||
"GITEA_PREFIX_SRC="+ctx.RenderOptions.Links.SrcLink(),
|
||||
"GITEA_PREFIX_RAW="+ctx.RenderOptions.Links.RawLink(),
|
||||
)
|
||||
if !p.IsInputFile {
|
||||
cmd.Stdin = input
|
||||
|
@ -25,9 +25,6 @@ const (
|
||||
IssueNameStyleRegexp = "regexp"
|
||||
)
|
||||
|
||||
// CSS class for action keywords (e.g. "closes: #1")
|
||||
const keywordClass = "issue-keyword"
|
||||
|
||||
type globalVarsType struct {
|
||||
hashCurrentPattern *regexp.Regexp
|
||||
shortLinkPattern *regexp.Regexp
|
||||
@ -39,6 +36,7 @@ type globalVarsType struct {
|
||||
emojiShortCodeRegex *regexp.Regexp
|
||||
issueFullPattern *regexp.Regexp
|
||||
filesChangedFullPattern *regexp.Regexp
|
||||
codePreviewPattern *regexp.Regexp
|
||||
|
||||
tagCleaner *regexp.Regexp
|
||||
nulCleaner *strings.Replacer
|
||||
@ -88,6 +86,9 @@ var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
|
||||
// example: https://domain/org/repo/pulls/27/files#hash
|
||||
v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
|
||||
|
||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||
|
||||
v.tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
|
||||
v.nulCleaner = strings.NewReplacer("\000", "")
|
||||
return v
|
||||
@ -129,7 +130,7 @@ func CustomLinkURLSchemes(schemes []string) {
|
||||
}
|
||||
withAuth = append(withAuth, s)
|
||||
}
|
||||
common.LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
|
||||
common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
|
||||
}
|
||||
|
||||
type postProcessError struct {
|
||||
@ -164,11 +165,7 @@ var defaultProcessors = []processor{
|
||||
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
|
||||
// MediaWiki, linking issues in the format #ID, and mentions in the format
|
||||
// @user, and others.
|
||||
func PostProcess(
|
||||
ctx *RenderContext,
|
||||
input io.Reader,
|
||||
output io.Writer,
|
||||
) error {
|
||||
func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
return postProcess(ctx, defaultProcessors, input, output)
|
||||
}
|
||||
|
||||
@ -189,10 +186,7 @@ var commitMessageProcessors = []processor{
|
||||
// RenderCommitMessage will use the same logic as PostProcess, but will disable
|
||||
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
|
||||
// set, which changes every text node into a link to the passed default link.
|
||||
func RenderCommitMessage(
|
||||
ctx *RenderContext,
|
||||
content string,
|
||||
) (string, error) {
|
||||
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) {
|
||||
procs := commitMessageProcessors
|
||||
return renderProcessString(ctx, procs, content)
|
||||
}
|
||||
@ -219,10 +213,7 @@ var emojiProcessors = []processor{
|
||||
// RenderCommitMessage, but will disable the shortLinkProcessor and
|
||||
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
|
||||
// which changes every text node into a link to the passed default link.
|
||||
func RenderCommitMessageSubject(
|
||||
ctx *RenderContext,
|
||||
defaultLink, content string,
|
||||
) (string, error) {
|
||||
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
|
||||
procs := slices.Clone(commitMessageSubjectProcessors)
|
||||
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
|
||||
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
|
||||
@ -236,10 +227,7 @@ func RenderCommitMessageSubject(
|
||||
}
|
||||
|
||||
// RenderIssueTitle to process title on individual issue/pull page
|
||||
func RenderIssueTitle(
|
||||
ctx *RenderContext,
|
||||
title string,
|
||||
) (string, error) {
|
||||
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) {
|
||||
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
|
||||
return renderProcessString(ctx, []processor{
|
||||
emojiShortCodeProcessor,
|
||||
@ -257,10 +245,7 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
|
||||
|
||||
// RenderDescriptionHTML will use similar logic as PostProcess, but will
|
||||
// use a single special linkProcessor.
|
||||
func RenderDescriptionHTML(
|
||||
ctx *RenderContext,
|
||||
content string,
|
||||
) (string, error) {
|
||||
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
|
||||
return renderProcessString(ctx, []processor{
|
||||
descriptionLinkProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
@ -270,10 +255,7 @@ func RenderDescriptionHTML(
|
||||
|
||||
// RenderEmoji for when we want to just process emoji and shortcodes
|
||||
// in various places it isn't already run through the normal markdown processor
|
||||
func RenderEmoji(
|
||||
ctx *RenderContext,
|
||||
content string,
|
||||
) (string, error) {
|
||||
func RenderEmoji(ctx *RenderContext, content string) (string, error) {
|
||||
return renderProcessString(ctx, emojiProcessors, content)
|
||||
}
|
||||
|
||||
@ -333,6 +315,17 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEmojiNode(node *html.Node) bool {
|
||||
if node.Type == html.ElementNode && node.Data == atom.Span.String() {
|
||||
for _, attr := range node.Attr {
|
||||
if (attr.Key == "class" || attr.Key == "data-attr-class") && strings.Contains(attr.Val, "emoji") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
|
||||
// Add user-content- to IDs and "#" links if they don't already have them
|
||||
for idx, attr := range node.Attr {
|
||||
@ -346,47 +339,27 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||
if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
|
||||
node.Attr[idx].Val = "#user-content-" + val
|
||||
}
|
||||
|
||||
if attr.Key == "class" && attr.Val == "emoji" {
|
||||
procs = nil
|
||||
}
|
||||
}
|
||||
|
||||
switch node.Type {
|
||||
case html.TextNode:
|
||||
processTextNodes(ctx, procs, node)
|
||||
for _, proc := range procs {
|
||||
proc(ctx, node) // it might add siblings
|
||||
}
|
||||
|
||||
case html.ElementNode:
|
||||
if node.Data == "code" || node.Data == "pre" {
|
||||
// ignore code and pre nodes
|
||||
if isEmojiNode(node) {
|
||||
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
|
||||
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
|
||||
return node.NextSibling
|
||||
} else if node.Data == "code" || node.Data == "pre" {
|
||||
return node.NextSibling // ignore code and pre nodes
|
||||
} else if node.Data == "img" {
|
||||
return visitNodeImg(ctx, node)
|
||||
} else if node.Data == "video" {
|
||||
return visitNodeVideo(ctx, node)
|
||||
} else if node.Data == "a" {
|
||||
// Restrict text in links to emojis
|
||||
procs = emojiProcessors
|
||||
} else if node.Data == "i" {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key != "class" {
|
||||
continue
|
||||
}
|
||||
classes := strings.Split(attr.Val, " ")
|
||||
for i, class := range classes {
|
||||
if class == "icon" {
|
||||
classes[0], classes[i] = classes[i], classes[0]
|
||||
attr.Val = strings.Join(classes, " ")
|
||||
|
||||
// Remove all children of icons
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
node.RemoveChild(child)
|
||||
child = node.FirstChild
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
procs = emojiProcessors // Restrict text in links to emojis
|
||||
}
|
||||
for n := node.FirstChild; n != nil; {
|
||||
n = visitNode(ctx, procs, n)
|
||||
@ -396,22 +369,17 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
||||
return node.NextSibling
|
||||
}
|
||||
|
||||
// processTextNodes runs the passed node through various processors, in order to handle
|
||||
// all kinds of special links handled by the post-processing.
|
||||
func processTextNodes(ctx *RenderContext, procs []processor, node *html.Node) {
|
||||
for _, p := range procs {
|
||||
p(ctx, node)
|
||||
}
|
||||
}
|
||||
|
||||
// createKeyword() renders a highlighted version of an action keyword
|
||||
func createKeyword(content string) *html.Node {
|
||||
func createKeyword(ctx *RenderContext, content string) *html.Node {
|
||||
// CSS class for action keywords (e.g. "closes: #1")
|
||||
const keywordClass = "issue-keyword"
|
||||
|
||||
span := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{},
|
||||
}
|
||||
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass})
|
||||
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", keywordClass))
|
||||
|
||||
text := &html.Node{
|
||||
Type: html.TextNode,
|
||||
@ -422,7 +390,7 @@ func createKeyword(content string) *html.Node {
|
||||
return span
|
||||
}
|
||||
|
||||
func createLink(href, content, class string) *html.Node {
|
||||
func createLink(ctx *RenderContext, href, content, class string) *html.Node {
|
||||
a := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.A.String(),
|
||||
@ -432,7 +400,7 @@ func createLink(href, content, class string) *html.Node {
|
||||
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
|
||||
}
|
||||
if class != "" {
|
||||
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
|
||||
a.Attr = append(a.Attr, ctx.RenderInternal.NodeSafeAttr("class", class))
|
||||
}
|
||||
|
||||
text := &html.Node{
|
||||
|
@ -6,7 +6,6 @@ package markup
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -16,9 +15,6 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||
var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||
|
||||
type RenderCodePreviewOptions struct {
|
||||
FullURL string
|
||||
OwnerName string
|
||||
@ -30,7 +26,7 @@ type RenderCodePreviewOptions struct {
|
||||
}
|
||||
|
||||
func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
|
||||
m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
m := globalVars().codePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
@ -42,7 +38,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
|
||||
CommitID: node.Data[m[6]:m[7]],
|
||||
FilePath: node.Data[m[8]:m[9]],
|
||||
}
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, opts.FullURL) {
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, opts.FullURL) {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
u, err := url.Parse(opts.FilePath)
|
||||
@ -55,7 +51,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
|
||||
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
|
||||
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
|
||||
opts.LineStart, opts.LineStop = lineStart, lineStop
|
||||
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
|
||||
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx, opts)
|
||||
return m[0], m[1], h, err
|
||||
}
|
||||
|
||||
@ -66,8 +62,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
|
||||
if err != nil || h == "" {
|
||||
urlPosStart, urlPosEnd, renderedCodeBlock, err := renderCodeBlock(ctx, node)
|
||||
if err != nil || renderedCodeBlock == "" {
|
||||
if err != nil {
|
||||
log.Error("Unable to render code preview: %v", err)
|
||||
}
|
||||
@ -84,7 +80,8 @@ func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
|
||||
// so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
|
||||
node.Data = textBefore
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
|
||||
renderedCodeNode := &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(renderedCodeBlock))}
|
||||
node.Parent.InsertBefore(renderedCodeNode, next)
|
||||
if textAfter != "" {
|
||||
node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
@ -23,10 +22,7 @@ func TestRenderCodePreview(t *testing.T) {
|
||||
},
|
||||
})
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
MarkupType: markdown.MarkupName,
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewRenderContext(context.Background()).WithMarkupType(markdown.MarkupName), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
||||
|
||||
// fullHashPatternProcessor renders SHA containing URLs
|
||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
@ -111,7 +111,7 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
@ -163,14 +163,14 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
|
||||
// are assumed to be in the same repository.
|
||||
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || (ctx.Repo == nil && ctx.GitRepo == nil) {
|
||||
if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || (ctx.RenderHelper.repoFacade == nil && ctx.RenderHelper.gitRepo == nil) {
|
||||
return
|
||||
}
|
||||
|
||||
start := 0
|
||||
next := node.NextSibling
|
||||
if ctx.ShaExistCache == nil {
|
||||
ctx.ShaExistCache = make(map[string]bool)
|
||||
if ctx.RenderHelper.shaExistCache == nil {
|
||||
ctx.RenderHelper.shaExistCache = make(map[string]bool)
|
||||
}
|
||||
for node != nil && node != next && start < len(node.Data) {
|
||||
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
|
||||
@ -191,25 +191,25 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// a commit in the repository before making it a link.
|
||||
|
||||
// check cache first
|
||||
exist, inCache := ctx.ShaExistCache[hash]
|
||||
exist, inCache := ctx.RenderHelper.shaExistCache[hash]
|
||||
if !inCache {
|
||||
if ctx.GitRepo == nil {
|
||||
if ctx.RenderHelper.gitRepo == nil {
|
||||
var err error
|
||||
var closer io.Closer
|
||||
ctx.GitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx.Ctx, ctx.Repo)
|
||||
ctx.RenderHelper.gitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx, ctx.RenderHelper.repoFacade)
|
||||
if err != nil {
|
||||
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.Repo), err)
|
||||
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.RenderHelper.repoFacade), err)
|
||||
return
|
||||
}
|
||||
ctx.AddCancel(func() {
|
||||
_ = closer.Close()
|
||||
ctx.GitRepo = nil
|
||||
ctx.RenderHelper.gitRepo = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
|
||||
exist = ctx.GitRepo.IsReferenceExist(hash)
|
||||
ctx.ShaExistCache[hash] = exist
|
||||
exist = ctx.RenderHelper.gitRepo.IsReferenceExist(hash)
|
||||
ctx.RenderHelper.shaExistCache[hash] = exist
|
||||
}
|
||||
|
||||
if !exist {
|
||||
@ -217,7 +217,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
continue
|
||||
}
|
||||
|
||||
link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
|
||||
link := util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash)
|
||||
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
|
||||
start = 0
|
||||
node = node.NextSibling.NextSibling
|
||||
|
@ -15,7 +15,7 @@ func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
mail := node.Data[m[2]:m[3]]
|
||||
replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
|
||||
replaceContent(node, m[2], m[3], createLink(ctx, "mailto:"+mail, mail, "" /*mailto*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,13 @@ import (
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
func createEmoji(content, class, name string) *html.Node {
|
||||
func createEmoji(ctx *RenderContext, content, name string) *html.Node {
|
||||
span := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{},
|
||||
}
|
||||
if class != "" {
|
||||
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
|
||||
}
|
||||
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji"))
|
||||
if name != "" {
|
||||
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name})
|
||||
}
|
||||
@ -35,13 +33,13 @@ func createEmoji(content, class, name string) *html.Node {
|
||||
return span
|
||||
}
|
||||
|
||||
func createCustomEmoji(alias string) *html.Node {
|
||||
func createCustomEmoji(ctx *RenderContext, alias string) *html.Node {
|
||||
span := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{},
|
||||
}
|
||||
span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: "emoji"})
|
||||
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji"))
|
||||
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
|
||||
|
||||
img := &html.Node{
|
||||
@ -77,7 +75,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if converted == nil {
|
||||
// check if this is a custom reaction
|
||||
if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
|
||||
replaceContent(node, m[0], m[1], createCustomEmoji(alias))
|
||||
replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
continue
|
||||
@ -85,7 +83,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
||||
continue
|
||||
}
|
||||
|
||||
replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
|
||||
replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
}
|
||||
@ -107,7 +105,7 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) {
|
||||
start = m[1]
|
||||
val := emoji.FromCode(codepoint)
|
||||
if val != nil {
|
||||
replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
|
||||
replaceContent(node, m[0], m[1], createEmoji(ctx, codepoint, val.Description))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
}
|
||||
|
@ -4,12 +4,12 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
testModule "code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -79,11 +79,11 @@ func TestRender_IssueIndexPattern(t *testing.T) {
|
||||
// numeric: render inputs without valid mentions
|
||||
test := func(s string) {
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
ctx: context.Background(),
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: numericMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: numericMetas},
|
||||
})
|
||||
}
|
||||
|
||||
@ -133,8 +133,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||
}
|
||||
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: localMetas},
|
||||
})
|
||||
|
||||
class := "ref-issue"
|
||||
@ -147,8 +147,8 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||
}
|
||||
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: numericMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: numericMetas},
|
||||
})
|
||||
}
|
||||
|
||||
@ -184,8 +184,8 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
|
||||
// alphanumeric: render inputs without valid mentions
|
||||
test := func(s string) {
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: alphanumericMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: alphanumericMetas},
|
||||
})
|
||||
}
|
||||
test("")
|
||||
@ -217,8 +217,8 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
|
||||
}
|
||||
expected := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: alphanumericMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: alphanumericMetas},
|
||||
})
|
||||
}
|
||||
test("OTT-1234 test", "%s test", "OTT-1234")
|
||||
@ -240,8 +240,8 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
|
||||
|
||||
expected := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: metas},
|
||||
})
|
||||
}
|
||||
|
||||
@ -264,8 +264,8 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
|
||||
)
|
||||
|
||||
testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: regexpMetas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: regexpMetas},
|
||||
})
|
||||
}
|
||||
|
||||
@ -279,16 +279,16 @@ func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
|
||||
}
|
||||
|
||||
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: metas},
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "#1312", "#1312", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: metas},
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "!1", "!1", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: metas},
|
||||
})
|
||||
}
|
||||
|
||||
@ -301,17 +301,17 @@ func TestRender_RenderIssueTitle(t *testing.T) {
|
||||
"style": IssueNameStyleNumeric,
|
||||
}
|
||||
actual, err := RenderIssueTitle(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
ctx: context.Background(),
|
||||
RenderOptions: RenderOptions{Metas: metas},
|
||||
}, "#1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "#1", actual)
|
||||
}
|
||||
|
||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
|
||||
ctx.Links.AbsolutePrefix = true
|
||||
if ctx.Links.Base == "" {
|
||||
ctx.Links.Base = TestRepoURL
|
||||
ctx.RenderOptions.Links.AbsolutePrefix = true
|
||||
if ctx.RenderOptions.Links.Base == "" {
|
||||
ctx.RenderOptions.Links.Base = TestRepoURL
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
@ -326,22 +326,18 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
test := func(input, expected string) {
|
||||
var buffer strings.Builder
|
||||
err := PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
ctx: context.Background(),
|
||||
|
||||
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
|
||||
}, strings.NewReader(input), &buffer)
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||
|
||||
buffer.Reset()
|
||||
err = PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
ctx: context.Background(),
|
||||
|
||||
RenderOptions: RenderOptions{Metas: localWikiMetas, Links: Links{Base: TestRepoURL}},
|
||||
}, strings.NewReader(input), &buffer)
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||
@ -368,11 +364,9 @@ func TestRender_FullIssueURLs(t *testing.T) {
|
||||
test := func(input, expected string) {
|
||||
var result strings.Builder
|
||||
err := postProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
ctx: context.Background(),
|
||||
|
||||
RenderOptions: RenderOptions{Metas: localMetas, Links: Links{Base: TestRepoURL}},
|
||||
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result.String())
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
next := node.NextSibling
|
||||
@ -36,14 +36,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
link := node.Data[m[0]:m[1]]
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) {
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, link) {
|
||||
return
|
||||
}
|
||||
text := "#" + node.Data[m[2]:m[3]]
|
||||
// if m[4] and m[5] is not -1, then link is to a comment
|
||||
// indicate that in the text by appending (comment)
|
||||
if m[4] != -1 && m[5] != -1 {
|
||||
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
text += " " + locale.TrString("repo.from_comment")
|
||||
} else {
|
||||
text += " (comment)"
|
||||
@ -56,25 +56,25 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
matchOrg := linkParts[len(linkParts)-4]
|
||||
matchRepo := linkParts[len(linkParts)-3]
|
||||
|
||||
if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
|
||||
replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
|
||||
if matchOrg == ctx.RenderOptions.Metas["user"] && matchRepo == ctx.RenderOptions.Metas["repo"] {
|
||||
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
||||
} else {
|
||||
text = matchOrg + "/" + matchRepo + text
|
||||
replaceContent(node, m[0], m[1], createLink(link, text, "ref-issue"))
|
||||
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
||||
}
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// crossLinkOnly: do not parse "#123", only parse "owner/repo#123"
|
||||
// if there is no repo in the context, then the "#123" format can't be parsed
|
||||
// old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true"
|
||||
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
|
||||
|
||||
var (
|
||||
found bool
|
||||
@ -84,20 +84,20 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
|
||||
for node != nil && node != next {
|
||||
_, hasExtTrackFormat := ctx.Metas["format"]
|
||||
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
|
||||
|
||||
// Repos with external issue trackers might still need to reference local PRs
|
||||
// We need to concern with the first one that shows up in the text, whichever it is
|
||||
isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
|
||||
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
|
||||
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
|
||||
|
||||
switch ctx.Metas["style"] {
|
||||
switch ctx.RenderOptions.Metas["style"] {
|
||||
case "", IssueNameStyleNumeric:
|
||||
found, ref = foundNumeric, refNumeric
|
||||
case IssueNameStyleAlphanumeric:
|
||||
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
||||
case IssueNameStyleRegexp:
|
||||
pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
|
||||
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -121,24 +121,24 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
var link *html.Node
|
||||
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
|
||||
if hasExtTrackFormat && !ref.IsPull {
|
||||
ctx.Metas["index"] = ref.Issue
|
||||
ctx.RenderOptions.Metas["index"] = ref.Issue
|
||||
|
||||
res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
|
||||
res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
|
||||
if err != nil {
|
||||
// here we could just log the error and continue the rendering
|
||||
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
|
||||
}
|
||||
|
||||
link = createLink(res, reftext, "ref-issue ref-external-issue")
|
||||
link = createLink(ctx, res, reftext, "ref-issue ref-external-issue")
|
||||
} else {
|
||||
// Path determines the type of link that will be rendered. It's unknown at this point whether
|
||||
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
|
||||
// Gitea will redirect on click as appropriate.
|
||||
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
|
||||
if ref.Owner == "" {
|
||||
link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
|
||||
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
|
||||
} else {
|
||||
link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
|
||||
link = createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// Decorate action keywords if actionable
|
||||
var keyword *html.Node
|
||||
if references.IsXrefActionable(ref, hasExtTrackFormat) {
|
||||
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
|
||||
keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
|
||||
} else {
|
||||
keyword = &html.Node{
|
||||
Type: html.TextNode,
|
||||
@ -177,7 +177,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
|
||||
link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
|
||||
link := createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
|
||||
|
||||
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
||||
node = node.NextSibling.NextSibling
|
||||
|
@ -19,15 +19,15 @@ import (
|
||||
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
|
||||
isAnchorFragment := link != "" && link[0] == '#'
|
||||
if !isAnchorFragment && !IsFullURLString(link) {
|
||||
linkBase := ctx.Links.Base
|
||||
linkBase := ctx.RenderOptions.Links.Base
|
||||
if ctx.IsMarkupContentWiki() {
|
||||
// no need to check if the link should be resolved as a wiki link or a wiki raw link
|
||||
// just use wiki link here, and it will be redirected to a wiki raw link if necessary
|
||||
linkBase = ctx.Links.WikiLink()
|
||||
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
||||
linkBase = ctx.RenderOptions.Links.WikiLink()
|
||||
} else if ctx.RenderOptions.Links.BranchPath != "" || ctx.RenderOptions.Links.TreePath != "" {
|
||||
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
||||
// and then this link will be handled by the "legacy-ref" code and be redirected to the default branch like "/owner/repo/src/branch/main/{the-file-path}"
|
||||
linkBase = ctx.Links.SrcLink()
|
||||
linkBase = ctx.RenderOptions.Links.SrcLink()
|
||||
}
|
||||
link, resolved = util.URLJoin(linkBase, link), true
|
||||
}
|
||||
@ -147,7 +147,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
if image {
|
||||
if !absoluteLink {
|
||||
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
|
||||
link = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
|
||||
}
|
||||
title := props["title"]
|
||||
if title == "" {
|
||||
@ -189,13 +189,13 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
func linkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := common.LinkRegex.FindStringIndex(node.Data)
|
||||
m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := node.Data[m[0]:m[1]]
|
||||
replaceContent(node, m[0], m[1], createLink(uri, uri, "link"))
|
||||
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
@ -204,7 +204,7 @@ func linkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := common.LinkRegex.FindStringIndex(node.Data)
|
||||
m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
@ -25,15 +25,15 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||
loc.Start += start
|
||||
loc.End += start
|
||||
mention := node.Data[loc.Start:loc.End]
|
||||
teams, ok := ctx.Metas["teams"]
|
||||
teams, ok := ctx.RenderOptions.Metas["teams"]
|
||||
// FIXME: util.URLJoin may not be necessary here:
|
||||
// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
|
||||
// is an AppSubURL link we can probably fallback to concatenation.
|
||||
// team mention should follow @orgName/teamName style
|
||||
if ok && strings.Contains(mention, "/") {
|
||||
mentionOrgAndTeam := strings.Split(mention, "/")
|
||||
if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
|
||||
if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), "org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "" /*mention*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
continue
|
||||
@ -43,8 +43,8 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
mentionedUsername := mention[1:]
|
||||
|
||||
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
|
||||
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx, mentionedUsername) {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.RenderOptions.Links.Prefix(), mentionedUsername), mention, "" /*mention*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
} else {
|
||||
|
@ -17,7 +17,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
}
|
||||
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
|
||||
// By default, the "<img>" tag should also be clickable,
|
||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||
@ -53,7 +53,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||
continue
|
||||
}
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
attr.Val = util.URLJoin(ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
node.Attr[i] = attr
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
@ -57,16 +56,10 @@ func newMockRepo(ownerName, repoName string) gitrepo.Repository {
|
||||
func TestRender_Commits(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas, newMockRepo(testRepoOwnerName, testRepoName), markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: markup.TestRepoURL,
|
||||
}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -112,15 +105,11 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", localMetas,
|
||||
markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: setting.AppSubURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -154,13 +143,7 @@ func TestRender_links(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -265,13 +248,7 @@ func TestRender_email(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
res, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
res, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
|
||||
}
|
||||
@ -338,13 +315,7 @@ func TestRender_emoji(t *testing.T) {
|
||||
|
||||
test := func(input, expected string) {
|
||||
expected = strings.ReplaceAll(expected, "&", "&")
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext("a.md", markup.Links{Base: markup.TestRepoURL}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -404,22 +375,10 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||
tree := util.URLJoin(markup.TestRepoURL, "src", "master")
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: markup.TestRepoURL, BranchPath: "master"}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
buffer, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, input)
|
||||
buffer, err = markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: markup.TestRepoURL}, localWikiMetas), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
@ -529,11 +488,7 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||
|
||||
func TestRender_RelativeMedias(t *testing.T) {
|
||||
render := func(input string, isWiki bool, links markup.Links) string {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: links,
|
||||
Metas: util.Iif(isWiki, localWikiMetas, localMetas),
|
||||
}, input)
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(links, util.Iif(isWiki, localWikiMetas, localMetas)), input)
|
||||
assert.NoError(t, err)
|
||||
return strings.TrimSpace(string(buffer))
|
||||
}
|
||||
@ -574,26 +529,14 @@ func Test_ParseClusterFuzz(t *testing.T) {
|
||||
data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, res.String(), "<html")
|
||||
|
||||
data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
|
||||
|
||||
res.Reset()
|
||||
err = markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err = markup.PostProcess(markup.NewTestRenderContext(markup.Links{Base: "https://example.com"}, localMetas), strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, res.String(), "<html")
|
||||
@ -606,14 +549,13 @@ func TestPostProcess_RenderDocument(t *testing.T) {
|
||||
|
||||
test := func(input, expected string) {
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
err := markup.PostProcess(markup.NewTestRenderContext(
|
||||
markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: map[string]string{"user": "go-gitea", "repo": "gitea"},
|
||||
}, strings.NewReader(input), &res)
|
||||
map[string]string{"user": "go-gitea", "repo": "gitea"},
|
||||
), strings.NewReader(input), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
|
||||
}
|
||||
@ -650,10 +592,7 @@ func TestIssue16020(t *testing.T) {
|
||||
data := `<img src="data:image/png;base64,i//V"/>`
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, data, res.String())
|
||||
}
|
||||
@ -666,29 +605,23 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzz(t *testing.T) {
|
||||
s := "t/l/issues/8#/../../a"
|
||||
renderContext := markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
renderContext := markup.NewTestRenderContext(
|
||||
markup.Links{
|
||||
Base: "https://example.com/go-gitea/gitea",
|
||||
},
|
||||
Metas: map[string]string{
|
||||
map[string]string{
|
||||
"user": "go-gitea",
|
||||
"repo": "gitea",
|
||||
},
|
||||
}
|
||||
|
||||
err := markup.PostProcess(&renderContext, strings.NewReader(s), io.Discard)
|
||||
|
||||
)
|
||||
err := markup.PostProcess(renderContext, strings.NewReader(s), io.Discard)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@ -696,10 +629,7 @@ func TestIssue18471(t *testing.T) {
|
||||
data := `http://domain/org/repo/compare/783b039...da951ce`
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcess(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
|
||||
|
30
modules/markup/internal/finalprocessor.go
Normal file
30
modules/markup/internal/finalprocessor.go
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type finalProcessor struct {
|
||||
renderInternal *RenderInternal
|
||||
|
||||
output io.Writer
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (p *finalProcessor) Write(data []byte) (int, error) {
|
||||
p.buf.Write(data)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (p *finalProcessor) Close() error {
|
||||
// TODO: reading the whole markdown isn't a problem at the moment,
|
||||
// because "postProcess" already does so. In the future we could optimize the code to process data on the fly.
|
||||
buf := p.buf.Bytes()
|
||||
buf = bytes.ReplaceAll(buf, []byte(` data-attr-class="`+p.renderInternal.secureIDPrefix), []byte(` class="`))
|
||||
_, err := p.output.Write(buf)
|
||||
return err
|
||||
}
|
61
modules/markup/internal/internal_test.go
Normal file
61
modules/markup/internal/internal_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderInternal(t *testing.T) {
|
||||
cases := []struct {
|
||||
input, protected, recovered string
|
||||
}{
|
||||
{
|
||||
input: `<div class="test">class="content"</div>`,
|
||||
protected: `<div data-attr-class="sec:test">class="content"</div>`,
|
||||
recovered: `<div class="test">class="content"</div>`,
|
||||
},
|
||||
{
|
||||
input: "<div\nclass=\"test\" data-xxx></div>",
|
||||
protected: `<div data-attr-class="sec:test" data-xxx></div>`,
|
||||
recovered: `<div class="test" data-xxx></div>`,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
var r RenderInternal
|
||||
out := &bytes.Buffer{}
|
||||
in := r.init("sec", out)
|
||||
protected := r.ProtectSafeAttrs(template.HTML(c.input))
|
||||
assert.EqualValues(t, c.protected, protected)
|
||||
_, _ = io.WriteString(in, string(protected))
|
||||
_ = in.Close()
|
||||
assert.EqualValues(t, c.recovered, out.String())
|
||||
}
|
||||
|
||||
var r1, r2 RenderInternal
|
||||
protected := r1.ProtectSafeAttrs(`<div class="test"></div>`)
|
||||
assert.EqualValues(t, `<div class="test"></div>`, protected, "non-initialized RenderInternal should not protect any attributes")
|
||||
_ = r1.init("sec", nil)
|
||||
protected = r1.ProtectSafeAttrs(`<div class="test"></div>`)
|
||||
assert.EqualValues(t, `<div data-attr-class="sec:test"></div>`, protected)
|
||||
assert.EqualValues(t, "data-attr-class", r1.SafeAttr("class"))
|
||||
assert.EqualValues(t, "sec:val", r1.SafeValue("val"))
|
||||
recovered, ok := r1.RecoverProtectedValue("sec:val")
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, "val", recovered)
|
||||
recovered, ok = r1.RecoverProtectedValue("other:val")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, recovered)
|
||||
|
||||
out2 := &bytes.Buffer{}
|
||||
in2 := r2.init("sec-other", out2)
|
||||
_, _ = io.WriteString(in2, string(protected))
|
||||
_ = in2.Close()
|
||||
assert.EqualValues(t, `<div data-attr-class="sec:test"></div>`, out2.String(), "different secureID should not recover the value")
|
||||
}
|
82
modules/markup/internal/renderinternal.go
Normal file
82
modules/markup/internal/renderinternal.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var reAttrClass = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
// TODO: it isn't a problem at the moment because our HTML contents are always well constructed
|
||||
return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`)
|
||||
})
|
||||
|
||||
// RenderInternal also works without initialization
|
||||
// If no initialization (no secureID), it will not protect any attributes and return the original name&value
|
||||
type RenderInternal struct {
|
||||
secureID string
|
||||
secureIDPrefix string
|
||||
}
|
||||
|
||||
func (r *RenderInternal) Init(output io.Writer) io.WriteCloser {
|
||||
buf := make([]byte, 12)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
panic("unable to generate secure id")
|
||||
}
|
||||
return r.init(base64.URLEncoding.EncodeToString(buf), output)
|
||||
}
|
||||
|
||||
func (r *RenderInternal) init(secID string, output io.Writer) io.WriteCloser {
|
||||
r.secureID = secID
|
||||
r.secureIDPrefix = r.secureID + ":"
|
||||
return &finalProcessor{renderInternal: r, output: output}
|
||||
}
|
||||
|
||||
func (r *RenderInternal) RecoverProtectedValue(v string) (string, bool) {
|
||||
if !strings.HasPrefix(v, r.secureIDPrefix) {
|
||||
return "", false
|
||||
}
|
||||
return v[len(r.secureIDPrefix):], true
|
||||
}
|
||||
|
||||
func (r *RenderInternal) SafeAttr(name string) string {
|
||||
if r.secureID == "" {
|
||||
return name
|
||||
}
|
||||
return "data-attr-" + name
|
||||
}
|
||||
|
||||
func (r *RenderInternal) SafeValue(val string) string {
|
||||
if r.secureID == "" {
|
||||
return val
|
||||
}
|
||||
return r.secureID + ":" + val
|
||||
}
|
||||
|
||||
func (r *RenderInternal) NodeSafeAttr(attr, val string) html.Attribute {
|
||||
return html.Attribute{Key: r.SafeAttr(attr), Val: r.SafeValue(val)}
|
||||
}
|
||||
|
||||
func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML {
|
||||
if r.secureID == "" {
|
||||
return content
|
||||
}
|
||||
return template.HTML(reAttrClass().ReplaceAllString(string(content), `$1 data-attr-class="`+r.secureIDPrefix+`$2"$3`))
|
||||
}
|
||||
|
||||
func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt string, a ...any) error {
|
||||
_, err := w.Write([]byte(r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...))))
|
||||
return err
|
||||
}
|
@ -34,13 +34,6 @@ func NewDetails() *Details {
|
||||
}
|
||||
}
|
||||
|
||||
// IsDetails returns true if the given node implements the Details interface,
|
||||
// otherwise false.
|
||||
func IsDetails(node ast.Node) bool {
|
||||
_, ok := node.(*Details)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Summary is a block that contains the summary of details block
|
||||
type Summary struct {
|
||||
ast.BaseBlock
|
||||
@ -66,13 +59,6 @@ func NewSummary() *Summary {
|
||||
}
|
||||
}
|
||||
|
||||
// IsSummary returns true if the given node implements the Summary interface,
|
||||
// otherwise false.
|
||||
func IsSummary(node ast.Node) bool {
|
||||
_, ok := node.(*Summary)
|
||||
return ok
|
||||
}
|
||||
|
||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
|
||||
type TaskCheckBoxListItem struct {
|
||||
*ast.ListItem
|
||||
@ -103,14 +89,7 @@ func NewTaskCheckBoxListItem(listItem *ast.ListItem) *TaskCheckBoxListItem {
|
||||
}
|
||||
}
|
||||
|
||||
// IsTaskCheckBoxListItem returns true if the given node implements the TaskCheckBoxListItem interface,
|
||||
// otherwise false.
|
||||
func IsTaskCheckBoxListItem(node ast.Node) bool {
|
||||
_, ok := node.(*TaskCheckBoxListItem)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Icon is an inline for a fomantic icon
|
||||
// Icon is an inline for a Fomantic UI icon
|
||||
type Icon struct {
|
||||
ast.BaseInline
|
||||
Name []byte
|
||||
@ -139,13 +118,6 @@ func NewIcon(name string) *Icon {
|
||||
}
|
||||
}
|
||||
|
||||
// IsIcon returns true if the given node implements the Icon interface,
|
||||
// otherwise false.
|
||||
func IsIcon(node ast.Node) bool {
|
||||
_, ok := node.(*Icon)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ColorPreview is an inline for a color preview
|
||||
type ColorPreview struct {
|
||||
ast.BaseInline
|
||||
|
@ -7,9 +7,11 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@ -23,11 +25,13 @@ import (
|
||||
|
||||
// ASTTransformer is a default transformer of the goldmark tree.
|
||||
type ASTTransformer struct {
|
||||
renderInternal *internal.RenderInternal
|
||||
attentionTypes container.Set[string]
|
||||
}
|
||||
|
||||
func NewASTTransformer() *ASTTransformer {
|
||||
func NewASTTransformer(renderInternal *internal.RenderInternal) *ASTTransformer {
|
||||
return &ASTTransformer{
|
||||
renderInternal: renderInternal,
|
||||
attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
|
||||
}
|
||||
}
|
||||
@ -75,7 +79,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
|
||||
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
|
||||
// especially in many tests.
|
||||
markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"]
|
||||
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
|
||||
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
|
||||
v.SetHardLineBreak(true)
|
||||
} else if markdownLineBreakStyle == "comment" {
|
||||
@ -109,12 +113,16 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render
|
||||
// in the gitea form.
|
||||
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
// it is copied from old code, which is quite doubtful whether it is correct
|
||||
var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
||||
})
|
||||
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
|
||||
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &HTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
reValidName: regexp.MustCompile("^[a-z ]+$"),
|
||||
renderInternal: renderInternal,
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
@ -126,7 +134,7 @@ func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
// renders gitea specific features.
|
||||
type HTMLRenderer struct {
|
||||
html.Config
|
||||
reValidName *regexp.Regexp
|
||||
renderInternal *internal.RenderInternal
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
@ -214,12 +222,13 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if !r.reValidName.MatchString(name) {
|
||||
if !reValidIconName().MatchString(name) {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
_, err := w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
|
||||
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
|
||||
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@ -29,11 +28,6 @@ import (
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var (
|
||||
specMarkdown goldmark.Markdown
|
||||
specMarkdownOnce sync.Once
|
||||
)
|
||||
|
||||
var (
|
||||
renderContextKey = parser.NewContextKey()
|
||||
renderConfigKey = parser.NewContextKey()
|
||||
@ -68,85 +62,95 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
|
||||
return pc
|
||||
}
|
||||
|
||||
// SpecializedMarkdown sets up the Gitea specific markdown extensions
|
||||
func SpecializedMarkdown() goldmark.Markdown {
|
||||
specMarkdownOnce.Do(func() {
|
||||
specMarkdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.NewTable(
|
||||
extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
|
||||
extension.Strikethrough,
|
||||
extension.TaskList,
|
||||
extension.DefinitionList,
|
||||
common.FootnoteExtension,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.WithClasses(true),
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
highlighting.WithWrapperRenderer(func(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
if entering {
|
||||
language, _ := c.Language()
|
||||
if language == nil {
|
||||
language = []byte("text")
|
||||
}
|
||||
type GlodmarkRender struct {
|
||||
ctx *markup.RenderContext
|
||||
|
||||
languageStr := string(language)
|
||||
|
||||
preClasses := []string{"code-block"}
|
||||
if languageStr == "mermaid" || languageStr == "math" {
|
||||
preClasses = append(preClasses, "is-loading")
|
||||
}
|
||||
|
||||
_, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// include language-x class as part of commonmark spec
|
||||
// the "display" class is used by "js/markup/math.js" to render the code element as a block
|
||||
_, err = w.WriteString(`<code class="chroma language-` + string(language) + ` display">`)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := w.WriteString("</code></pre>")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
math.NewExtension(
|
||||
math.Enabled(setting.Markdown.EnableMath),
|
||||
),
|
||||
meta.Meta,
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAttribute(),
|
||||
parser.WithAutoHeadingID(),
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(NewASTTransformer(), 10000),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
)
|
||||
|
||||
// Override the original Tasklist renderer!
|
||||
specMarkdown.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewHTMLRenderer(), 10),
|
||||
),
|
||||
)
|
||||
})
|
||||
return specMarkdown
|
||||
goldmarkMarkdown goldmark.Markdown
|
||||
}
|
||||
|
||||
// actualRender renders Markdown to HTML without handling special links.
|
||||
func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
converter := SpecializedMarkdown()
|
||||
func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
|
||||
return r.goldmarkMarkdown.Convert(source, writer, opts...)
|
||||
}
|
||||
|
||||
func (r *GlodmarkRender) Renderer() renderer.Renderer {
|
||||
return r.goldmarkMarkdown.Renderer()
|
||||
}
|
||||
|
||||
func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
if entering {
|
||||
language, _ := c.Language()
|
||||
if language == nil {
|
||||
language = []byte("text")
|
||||
}
|
||||
|
||||
languageStr := string(language)
|
||||
|
||||
preClasses := []string{"code-block"}
|
||||
if languageStr == "mermaid" || languageStr == "math" {
|
||||
preClasses = append(preClasses, "is-loading")
|
||||
}
|
||||
|
||||
err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, strings.Join(preClasses, " "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// include language-x class as part of commonmark spec
|
||||
// the "display" class is used by "js/markup/math.js" to render the code element as a block
|
||||
err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, string(language))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := w.WriteString("</code></pre>")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SpecializedMarkdown sets up the Gitea specific markdown extensions
|
||||
func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
|
||||
// TODO: it could use a pool to cache the renderers to reuse them with different contexts
|
||||
// at the moment it is fast enough (see the benchmarks)
|
||||
r := &GlodmarkRender{ctx: ctx}
|
||||
r.goldmarkMarkdown = goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
|
||||
extension.Strikethrough,
|
||||
extension.TaskList,
|
||||
extension.DefinitionList,
|
||||
common.FootnoteExtension,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.WithClasses(true),
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
highlighting.WithWrapperRenderer(r.highlightingRenderer),
|
||||
),
|
||||
math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)),
|
||||
meta.Meta,
|
||||
),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAttribute(),
|
||||
parser.WithAutoHeadingID(),
|
||||
parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
|
||||
),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||
)
|
||||
|
||||
// Override the original Tasklist renderer!
|
||||
r.goldmarkMarkdown.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
|
||||
)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// render calls goldmark render to convert Markdown to HTML
|
||||
// NOTE: The output of this method MUST get sanitized separately!!!
|
||||
func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
converter := SpecializedMarkdown(ctx)
|
||||
lw := &limitWriter{
|
||||
w: output,
|
||||
limit: setting.UI.MaxDisplayFileSize * 3,
|
||||
@ -160,8 +164,8 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
|
||||
}
|
||||
|
||||
log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
|
||||
if log.IsDebug() {
|
||||
log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
|
||||
if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() {
|
||||
log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
|
||||
@ -178,7 +182,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
|
||||
bufWithMetadataLength := len(buf)
|
||||
|
||||
rc := &RenderConfig{
|
||||
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
|
||||
Meta: markup.RenderMetaAsDetails,
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
@ -200,26 +204,6 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: The output of this method must get sanitized.
|
||||
func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn("Unable to render markdown due to panic in goldmark - will return raw bytes")
|
||||
if log.IsDebug() {
|
||||
log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
|
||||
}
|
||||
_, err = io.Copy(output, input)
|
||||
if err != nil {
|
||||
log.Error("io.Copy failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return actualRender(ctx, input, output)
|
||||
}
|
||||
|
||||
// MarkupName describes markup's name
|
||||
var MarkupName = "markdown"
|
||||
|
||||
@ -257,7 +241,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
|
||||
|
||||
// Render renders Markdown to HTML with all specific handling stuff.
|
||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
ctx.MarkupType = MarkupName
|
||||
ctx.RenderOptions.MarkupType = MarkupName
|
||||
return markup.Render(ctx, input, output)
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,10 @@
|
||||
package markdown_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@ -67,22 +65,11 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
|
||||
buffer, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, input)
|
||||
buffer, err = markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}, localWikiMetas), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
@ -101,12 +88,7 @@ func TestRender_Images(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
@ -308,14 +290,11 @@ func TestTotal_RenderWiki(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
|
||||
for i := 0; i < len(sameCases); i++ {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localWikiMetas,
|
||||
}, sameCases[i])
|
||||
line, err := markdown.RenderString(markup.NewTestRenderContext(
|
||||
markup.Links{Base: FullURL},
|
||||
newMockRepo(testRepoOwnerName, testRepoName),
|
||||
localWikiMetas,
|
||||
), sameCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, answers[i], string(line))
|
||||
}
|
||||
@ -334,13 +313,7 @@ func TestTotal_RenderWiki(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, testCases[i])
|
||||
line, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}, localWikiMetas), testCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, testCases[i+1], string(line))
|
||||
}
|
||||
@ -352,15 +325,14 @@ func TestTotal_RenderString(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
|
||||
for i := 0; i < len(sameCases); i++ {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
line, err := markdown.RenderString(markup.NewTestRenderContext(
|
||||
markup.Links{
|
||||
Base: FullURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localMetas,
|
||||
}, sameCases[i])
|
||||
newMockRepo(testRepoOwnerName, testRepoName),
|
||||
localMetas,
|
||||
), sameCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, answers[i], string(line))
|
||||
}
|
||||
@ -368,12 +340,7 @@ func TestTotal_RenderString(t *testing.T) {
|
||||
testCases := []string{}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, testCases[i])
|
||||
line, err := markdown.RenderString(markup.NewTestRenderContext(markup.Links{Base: FullURL}), testCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, template.HTML(testCases[i+1]), line)
|
||||
}
|
||||
@ -381,17 +348,17 @@ func TestTotal_RenderString(t *testing.T) {
|
||||
|
||||
func TestRender_RenderParagraphs(t *testing.T) {
|
||||
test := func(t *testing.T, str string, cnt int) {
|
||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, str)
|
||||
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), str)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
|
||||
mac := strings.ReplaceAll(str, "\n", "\r")
|
||||
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, mac)
|
||||
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), mac)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
|
||||
dos := strings.ReplaceAll(str, "\n", "\r\n")
|
||||
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, dos)
|
||||
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), dos)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
}
|
||||
@ -419,7 +386,7 @@ func TestMarkdownRenderRaw(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
log.Info("Test markdown render error with fuzzy data: %x, the following errors can be recovered", testcase)
|
||||
_, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, string(testcase))
|
||||
_, err := markdown.RenderRawString(markup.NewTestRenderContext(), string(testcase))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@ -432,7 +399,7 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
|
||||
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
|
||||
`
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, res)
|
||||
}
|
||||
@ -441,7 +408,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
|
||||
testcase := `[Link with emoji :moon: in text](https://gitea.io)`
|
||||
expected := `<p><a href="https://gitea.io" rel="nofollow">Link with emoji <span class="emoji" aria-label="waxing gibbous moon">🌔</span> in text</a></p>
|
||||
`
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, template.HTML(expected), res)
|
||||
}
|
||||
@ -479,7 +446,7 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range positiveTests {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -498,7 +465,7 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range negativeTests {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test)
|
||||
assert.NotContains(t, res, `<span class="color-preview" style="background-color: `, "Unexpected result in testcase %q", test)
|
||||
}
|
||||
@ -573,7 +540,7 @@ func TestMathBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -610,7 +577,7 @@ foo: bar
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -1003,11 +970,7 @@ space</p>
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
for i, c := range cases {
|
||||
result, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: context.Background(),
|
||||
Links: c.Links,
|
||||
Metas: util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{}),
|
||||
}, input)
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(c.Links, util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{})), input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
||||
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
|
||||
}
|
||||
@ -1029,7 +992,7 @@ func TestAttention(t *testing.T) {
|
||||
}
|
||||
|
||||
test := func(input, expected string) {
|
||||
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
|
||||
}
|
||||
@ -1051,3 +1014,17 @@ func TestAttention(t *testing.T) {
|
||||
// legacy GitHub style
|
||||
test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
}
|
||||
|
||||
func BenchmarkSpecializedMarkdown(b *testing.B) {
|
||||
// 240856 4719 ns/op
|
||||
for i := 0; i < b.N; i++ {
|
||||
markdown.SpecializedMarkdown(&markup.RenderContext{})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarkdownRender(b *testing.B) {
|
||||
// 23202 50840 ns/op
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = markdown.RenderString(markup.NewTestRenderContext(), "https://example.com\n- a\n- b\n")
|
||||
}
|
||||
}
|
||||
|
@ -4,17 +4,21 @@
|
||||
package math
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// BlockRenderer represents a renderer for math Blocks
|
||||
type BlockRenderer struct{}
|
||||
type BlockRenderer struct {
|
||||
renderInternal *internal.RenderInternal
|
||||
}
|
||||
|
||||
// NewBlockRenderer creates a new renderer for math Blocks
|
||||
func NewBlockRenderer() renderer.NodeRenderer {
|
||||
return &BlockRenderer{}
|
||||
func NewBlockRenderer(renderInternal *internal.RenderInternal) renderer.NodeRenderer {
|
||||
return &BlockRenderer{renderInternal: renderInternal}
|
||||
}
|
||||
|
||||
// RegisterFuncs registers the renderer for math Blocks
|
||||
@ -33,7 +37,7 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node)
|
||||
func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
n := node.(*Block)
|
||||
if entering {
|
||||
_, _ = w.WriteString(`<pre class="code-block is-loading"><code class="chroma language-math display">`)
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<pre class="code-block is-loading"><code class="chroma language-math display">`)
|
||||
r.writeLines(w, source, n)
|
||||
} else {
|
||||
_, _ = w.WriteString(`</code></pre>` + "\n")
|
||||
|
@ -6,17 +6,21 @@ package math
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// InlineRenderer is an inline renderer
|
||||
type InlineRenderer struct{}
|
||||
type InlineRenderer struct {
|
||||
renderInternal *internal.RenderInternal
|
||||
}
|
||||
|
||||
// NewInlineRenderer returns a new renderer for inline math
|
||||
func NewInlineRenderer() renderer.NodeRenderer {
|
||||
return &InlineRenderer{}
|
||||
func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRenderer {
|
||||
return &InlineRenderer{renderInternal: renderInternal}
|
||||
}
|
||||
|
||||
func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
@ -25,7 +29,7 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod
|
||||
if _, ok := n.(*InlineBlock); ok {
|
||||
extraClass = "display "
|
||||
}
|
||||
_, _ = w.WriteString(`<code class="language-math ` + extraClass + `is-loading">`)
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<code class="language-math %sis-loading">`, extraClass)
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
segment := c.(*ast.Text).Segment
|
||||
value := util.EscapeHTML(segment.Value(source))
|
||||
|
@ -4,6 +4,8 @@
|
||||
package math
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
@ -12,6 +14,7 @@ import (
|
||||
|
||||
// Extension is a math extension
|
||||
type Extension struct {
|
||||
renderInternal *internal.RenderInternal
|
||||
enabled bool
|
||||
parseDollarInline bool
|
||||
parseDollarBlock bool
|
||||
@ -39,38 +42,10 @@ func Enabled(enable ...bool) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// WithInlineDollarParser enables or disables the parsing of $...$
|
||||
func WithInlineDollarParser(enable ...bool) Option {
|
||||
value := true
|
||||
if len(enable) > 0 {
|
||||
value = enable[0]
|
||||
}
|
||||
return extensionFunc(func(e *Extension) {
|
||||
e.parseDollarInline = value
|
||||
})
|
||||
}
|
||||
|
||||
// WithBlockDollarParser enables or disables the parsing of $$...$$
|
||||
func WithBlockDollarParser(enable ...bool) Option {
|
||||
value := true
|
||||
if len(enable) > 0 {
|
||||
value = enable[0]
|
||||
}
|
||||
return extensionFunc(func(e *Extension) {
|
||||
e.parseDollarBlock = value
|
||||
})
|
||||
}
|
||||
|
||||
// Math represents a math extension with default rendered delimiters
|
||||
var Math = &Extension{
|
||||
enabled: true,
|
||||
parseDollarBlock: true,
|
||||
parseDollarInline: true,
|
||||
}
|
||||
|
||||
// NewExtension creates a new math extension with the provided options
|
||||
func NewExtension(opts ...Option) *Extension {
|
||||
func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension {
|
||||
r := &Extension{
|
||||
renderInternal: renderInternal,
|
||||
enabled: true,
|
||||
parseDollarBlock: true,
|
||||
parseDollarInline: true,
|
||||
@ -102,7 +77,7 @@ func (e *Extension) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
|
||||
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewBlockRenderer(), 501),
|
||||
util.Prioritized(NewInlineRenderer(), 502),
|
||||
util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
|
||||
util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
|
||||
))
|
||||
}
|
||||
|
@ -11,10 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
/*
|
||||
IssueTemplate is a legacy to keep the unit tests working.
|
||||
Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
|
||||
*/
|
||||
// IssueTemplate is a legacy to keep the unit tests working.
|
||||
// Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template.
|
||||
type IssueTemplate struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
|
@ -32,7 +32,8 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
|
||||
default: // including "note"
|
||||
octiconName = "info"
|
||||
}
|
||||
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
|
||||
svgHTML := svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)
|
||||
_, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(svgHTML)))
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
@ -128,13 +129,13 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
|
||||
}
|
||||
|
||||
// color the blockquote
|
||||
v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
|
||||
v.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-header attention-"+attentionType)))
|
||||
|
||||
// create an emphasis to make it bold
|
||||
attentionParagraph := ast.NewParagraph()
|
||||
g.applyElementDir(attentionParagraph)
|
||||
emphasis := ast.NewEmphasis(2)
|
||||
emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
|
||||
emphasis.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("attention-"+attentionType)))
|
||||
|
||||
attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
|
||||
|
||||
|
@ -5,7 +5,6 @@ package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@ -40,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
|
||||
r.Writer.RawWrite(w, value)
|
||||
}
|
||||
case *ColorPreview:
|
||||
_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<span class="color-preview" style="background-color: %s"></span>`, string(v.Color))
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
|
@ -21,7 +21,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
|
||||
// Check if the destination is a real link
|
||||
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
||||
v.Destination = []byte(giteautil.URLJoin(
|
||||
ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
|
||||
ctx.RenderOptions.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
|
||||
strings.TrimLeft(string(v.Destination), "/"),
|
||||
))
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc
|
||||
}
|
||||
newChild := NewTaskCheckBoxListItem(listItem)
|
||||
newChild.IsChecked = taskCheckBox.IsChecked
|
||||
newChild.SetAttributeString("class", []byte("task-list-item"))
|
||||
newChild.SetAttributeString(g.renderInternal.SafeAttr("class"), []byte(g.renderInternal.SafeValue("task-list-item")))
|
||||
segments := newChild.FirstChild().Lines()
|
||||
if segments.Len() > 0 {
|
||||
segment := segments.At(0)
|
||||
|
@ -143,15 +143,15 @@ func (r *Writer) resolveLink(kind, link string) string {
|
||||
kind = org.RegularLink{URL: link}.Kind()
|
||||
}
|
||||
|
||||
base := r.Ctx.Links.Base
|
||||
base := r.Ctx.RenderOptions.Links.Base
|
||||
if r.Ctx.IsMarkupContentWiki() {
|
||||
base = r.Ctx.Links.WikiLink()
|
||||
} else if r.Ctx.Links.HasBranchInfo() {
|
||||
base = r.Ctx.Links.SrcLink()
|
||||
base = r.Ctx.RenderOptions.Links.WikiLink()
|
||||
} else if r.Ctx.RenderOptions.Links.HasBranchInfo() {
|
||||
base = r.Ctx.RenderOptions.Links.SrcLink()
|
||||
}
|
||||
|
||||
if kind == "image" || kind == "video" {
|
||||
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
|
||||
base = r.Ctx.RenderOptions.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
|
||||
}
|
||||
|
||||
link = util.URLJoin(base, link)
|
||||
|
@ -4,10 +4,10 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -15,20 +15,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const AppURL = "http://localhost:3000/"
|
||||
func TestMain(m *testing.M) {
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
setting.IsInTesting = true
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRender_StandardLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string, isWiki bool) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
buffer, err := RenderString(markup.NewTestRenderContext(
|
||||
markup.Links{
|
||||
Base: "/relative-path",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
|
||||
}, input)
|
||||
map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
|
||||
), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -42,16 +43,13 @@ func TestRender_StandardLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_InternalLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
buffer, err := RenderString(markup.NewTestRenderContext(
|
||||
markup.Links{
|
||||
Base: "/relative-path",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
}, input)
|
||||
), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -67,15 +65,8 @@ func TestRender_InternalLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_Media(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "./relative-path",
|
||||
},
|
||||
}, input)
|
||||
buffer, err := RenderString(markup.NewTestRenderContext(markup.Links{Base: "./relative-path"}), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -113,12 +104,8 @@ func TestRender_Media(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_Source(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
}, input)
|
||||
buffer, err := RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
@ -9,14 +9,16 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type RenderMetaMode string
|
||||
@ -41,16 +43,16 @@ var RenderBehaviorForTesting struct {
|
||||
DisableInternalAttributes bool
|
||||
}
|
||||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
Ctx context.Context
|
||||
RelativePath string // relative path from tree root of the branch
|
||||
type RenderOptions struct {
|
||||
// relative path from tree root of the branch
|
||||
RelativePath string
|
||||
|
||||
// eg: "orgmode", "asciicast", "console"
|
||||
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
|
||||
MarkupType string
|
||||
|
||||
Links Links // special link references for rendering, especially when there is a branch/tree path
|
||||
// special link references for rendering, especially when there is a branch/tree path
|
||||
Links Links
|
||||
|
||||
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
|
||||
// BranchNameSubURL (for iframe&asciicast)
|
||||
@ -58,13 +60,83 @@ type RenderContext struct {
|
||||
// markdownLineBreakStyle (comment, document)
|
||||
Metas map[string]string
|
||||
|
||||
GitRepo *git.Repository
|
||||
Repo gitrepo.Repository
|
||||
ShaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
SidebarTocNode ast.Node
|
||||
RenderMetaAs RenderMetaMode
|
||||
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
InStandalonePage bool
|
||||
}
|
||||
|
||||
type RenderHelper struct {
|
||||
gitRepo *git.Repository
|
||||
repoFacade gitrepo.Repository
|
||||
shaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
}
|
||||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
ctx context.Context
|
||||
|
||||
SidebarTocNode ast.Node
|
||||
|
||||
RenderHelper RenderHelper
|
||||
RenderOptions RenderOptions
|
||||
RenderInternal internal.RenderInternal
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return ctx.ctx.Deadline()
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) Done() <-chan struct{} {
|
||||
return ctx.ctx.Done()
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) Err() error {
|
||||
return ctx.ctx.Err()
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) Value(key any) any {
|
||||
return ctx.ctx.Value(key)
|
||||
}
|
||||
|
||||
var _ context.Context = (*RenderContext)(nil)
|
||||
|
||||
func NewRenderContext(ctx context.Context) *RenderContext {
|
||||
return &RenderContext{ctx: ctx}
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext {
|
||||
ctx.RenderOptions.MarkupType = typ
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithRelativePath(path string) *RenderContext {
|
||||
ctx.RenderOptions.RelativePath = path
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithLinks(links Links) *RenderContext {
|
||||
ctx.RenderOptions.Links = links
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext {
|
||||
ctx.RenderOptions.Metas = metas
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext {
|
||||
ctx.RenderOptions.InStandalonePage = v
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithGitRepo(r *git.Repository) *RenderContext {
|
||||
ctx.RenderHelper.gitRepo = r
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithRepoFacade(r gitrepo.Repository) *RenderContext {
|
||||
ctx.RenderHelper.repoFacade = r
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Cancel runs any cleanup functions that have been registered for this Ctx
|
||||
@ -72,11 +144,11 @@ func (ctx *RenderContext) Cancel() {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
ctx.ShaExistCache = map[string]bool{}
|
||||
if ctx.cancelFn == nil {
|
||||
ctx.RenderHelper.shaExistCache = map[string]bool{}
|
||||
if ctx.RenderHelper.cancelFn == nil {
|
||||
return
|
||||
}
|
||||
ctx.cancelFn()
|
||||
ctx.RenderHelper.cancelFn()
|
||||
}
|
||||
|
||||
// AddCancel adds the provided fn as a Cleanup for this Ctx
|
||||
@ -84,38 +156,38 @@ func (ctx *RenderContext) AddCancel(fn func()) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
oldCancelFn := ctx.cancelFn
|
||||
oldCancelFn := ctx.RenderHelper.cancelFn
|
||||
if oldCancelFn == nil {
|
||||
ctx.cancelFn = fn
|
||||
ctx.RenderHelper.cancelFn = fn
|
||||
return
|
||||
}
|
||||
ctx.cancelFn = func() {
|
||||
ctx.RenderHelper.cancelFn = func() {
|
||||
defer oldCancelFn()
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) IsMarkupContentWiki() bool {
|
||||
return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
|
||||
return ctx.RenderOptions.Metas != nil && ctx.RenderOptions.Metas["markupContentMode"] == "wiki"
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.MarkupType == "" && ctx.RelativePath != "" {
|
||||
ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath)
|
||||
if ctx.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
|
||||
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
renderer := renderers[ctx.MarkupType]
|
||||
renderer := renderers[ctx.RenderOptions.MarkupType]
|
||||
if renderer == nil {
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType)
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
}
|
||||
|
||||
if ctx.RelativePath != "" {
|
||||
if ctx.RenderOptions.RelativePath != "" {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||
if !ctx.InStandalonePage {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||
return renderIFrame(ctx, output)
|
||||
@ -148,67 +220,61 @@ width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
|
||||
sandbox="allow-scripts"
|
||||
></iframe>`,
|
||||
setting.AppSubURL,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
ctx.RenderOptions.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RenderOptions.RelativePath),
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
func pipes() (io.ReadCloser, io.WriteCloser, func()) {
|
||||
pr, pw := io.Pipe()
|
||||
defer func() {
|
||||
return pr, pw, func() {
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var pr2 io.ReadCloser
|
||||
var pw2 io.WriteCloser
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
finalProcessor := ctx.RenderInternal.Init(output)
|
||||
defer finalProcessor.Close()
|
||||
|
||||
var sanitizerDisabled bool
|
||||
if r, ok := renderer.(ExternalRenderer); ok {
|
||||
sanitizerDisabled = r.SanitizerDisabled()
|
||||
// input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
|
||||
// no sanitizer: input -> (pw1=pr1) -> renderer -> pw2(finalProcessor) -> output
|
||||
pr1, pw1, close1 := pipes()
|
||||
defer close1()
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
|
||||
|
||||
if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
|
||||
var pr2 io.ReadCloser
|
||||
var close2 func()
|
||||
pr2, pw2, close2 = pipes()
|
||||
defer close2()
|
||||
eg.Go(func() error {
|
||||
defer pr2.Close()
|
||||
return SanitizeReader(pr2, renderer.Name(), finalProcessor)
|
||||
})
|
||||
}
|
||||
|
||||
if !sanitizerDisabled {
|
||||
pr2, pw2 = io.Pipe()
|
||||
defer func() {
|
||||
_ = pr2.Close()
|
||||
_ = pw2.Close()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = SanitizeReader(pr2, renderer.Name(), output)
|
||||
_ = pr2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
} else {
|
||||
pw2 = util.NopCloser{Writer: output}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
eg.Go(func() (err error) {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, pr, pw2)
|
||||
err = PostProcess(ctx, pr1, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr)
|
||||
_, err = io.Copy(pw2, pr1)
|
||||
}
|
||||
_ = pr.Close()
|
||||
_ = pw2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
_, _ = pr1.Close(), pw2.Close()
|
||||
return err
|
||||
})
|
||||
|
||||
if err1 := renderer.Render(ctx, input, pw); err1 != nil {
|
||||
return err1
|
||||
if err := renderer.Render(ctx, input, pw1); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = pw.Close()
|
||||
_ = pw1.Close()
|
||||
|
||||
wg.Wait()
|
||||
return err
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
// Init initializes the render global variables
|
||||
@ -233,3 +299,27 @@ func Init(ph *ProcessorHelper) {
|
||||
func ComposeSimpleDocumentMetas() map[string]string {
|
||||
return map[string]string{"markdownLineBreakStyle": "document"}
|
||||
}
|
||||
|
||||
// NewTestRenderContext is a helper function to create a RenderContext for testing purpose
|
||||
// It accepts string (RelativePath), Links, map[string]string (Metas), gitrepo.Repository
|
||||
func NewTestRenderContext(a ...any) *RenderContext {
|
||||
if !setting.IsInTesting {
|
||||
panic("NewTestRenderContext should only be used in testing")
|
||||
}
|
||||
ctx := NewRenderContext(context.Background())
|
||||
for _, v := range a {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
ctx = ctx.WithRelativePath(v)
|
||||
case Links:
|
||||
ctx = ctx.WithLinks(v)
|
||||
case map[string]string:
|
||||
ctx = ctx.WithMetas(v)
|
||||
case gitrepo.Repository:
|
||||
ctx = ctx.WithRepoFacade(v)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %T", v))
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
@ -4,6 +4,9 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
@ -15,8 +18,11 @@ func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []settin
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
if rule.Regexp != "" {
|
||||
if !strings.HasPrefix(rule.Regexp, "^") || !strings.HasSuffix(rule.Regexp, "$") {
|
||||
panic("Markup sanitizer rule regexp must start with ^ and end with $ to be strict")
|
||||
}
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(regexp.MustCompile(rule.Regexp)).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
}
|
||||
|
@ -16,37 +16,12 @@ import (
|
||||
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
// NOTICE: DO NOT add special "class" regexp rules here anymore, use RenderInternal.SafeAttr instead
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
// General safe SVG attributes
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden", "data-attr-class").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
@ -66,28 +41,15 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
// Allow generally safe attributes (reference: https://github.com/jch/html-pipeline)
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
@ -106,10 +68,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
"data-markdown-generated-content",
|
||||
"vspace", "width", "itemprop", "itemscope", "itemtype",
|
||||
"data-markdown-generated-content", "data-attr-class",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
@ -117,14 +78,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
// Custom keyword markup
|
||||
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
@ -19,7 +19,6 @@ func TestSanitizer(t *testing.T) {
|
||||
// Code highlighting class
|
||||
`<code class="random string"></code>`, `<code></code>`,
|
||||
`<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`,
|
||||
`<code class="language-go"></code>`, `<code class="language-go"></code>`,
|
||||
|
||||
// Input checkbox
|
||||
`<input type="hidden">`, ``,
|
||||
@ -38,10 +37,8 @@ func TestSanitizer(t *testing.T) {
|
||||
// <kbd> tags
|
||||
`<kbd>Ctrl + C</kbd>`, `<kbd>Ctrl + C</kbd>`,
|
||||
`<i class="dropdown icon">NAUGHTY</i>`, `<i>NAUGHTY</i>`,
|
||||
`<i class="icon dropdown"></i>`, `<i class="icon dropdown"></i>`,
|
||||
`<input type="checkbox" disabled=""/>unchecked`, `<input type="checkbox" disabled=""/>unchecked`,
|
||||
`<span class="emoji dropdown">NAUGHTY</span>`, `<span>NAUGHTY</span>`,
|
||||
`<span class="emoji">contents</span>`, `<span class="emoji">contents</span>`,
|
||||
|
||||
// Color property
|
||||
`<span style="color: red">Hello World</span>`, `<span style="color: red">Hello World</span>`,
|
||||
|
@ -4,8 +4,6 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@ -125,15 +123,12 @@ func TestPushCommits_AvatarLink(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
setting.GravatarSource = "https://secure.gravatar.com/avatar"
|
||||
setting.OfflineMode = true
|
||||
|
||||
assert.Equal(t,
|
||||
"/avatars/avatar2?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
|
||||
"/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor),
|
||||
pushCommits.AvatarLink(db.DefaultContext, "user2@example.com"))
|
||||
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("https://secure.gravatar.com/avatar/%x?d=identicon&s=%d", md5.Sum([]byte("nonexistent@example.com")), 28*setting.Avatar.RenderedSizeFactor),
|
||||
"/assets/img/avatar_default.png",
|
||||
pushCommits.AvatarLink(db.DefaultContext, "nonexistent@example.com"))
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ type MarkupRenderer struct {
|
||||
type MarkupSanitizerRule struct {
|
||||
Element string
|
||||
AllowAttr string
|
||||
Regexp *regexp.Regexp
|
||||
Regexp string
|
||||
AllowDataURIImages bool
|
||||
}
|
||||
|
||||
@ -117,15 +117,24 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR
|
||||
|
||||
regexpStr := sec.Key("REGEXP").Value()
|
||||
if regexpStr != "" {
|
||||
// Validate when parsing the config that this is a valid regular
|
||||
// expression. Then we can use regexp.MustCompile(...) later.
|
||||
compiled, err := regexp.Compile(regexpStr)
|
||||
hasPrefix := strings.HasPrefix(regexpStr, "^")
|
||||
hasSuffix := strings.HasSuffix(regexpStr, "$")
|
||||
if !hasPrefix || !hasSuffix {
|
||||
log.Error("In markup.%s: REGEXP must start with ^ and end with $ to be strict", name)
|
||||
// to avoid breaking existing user configurations and satisfy the strict requirement in addSanitizerRules
|
||||
if !hasPrefix {
|
||||
regexpStr = "^.*" + regexpStr
|
||||
}
|
||||
if !hasSuffix {
|
||||
regexpStr += ".*$"
|
||||
}
|
||||
}
|
||||
_, err := regexp.Compile(regexpStr)
|
||||
if err != nil {
|
||||
log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
|
||||
return rule, false
|
||||
}
|
||||
|
||||
rule.Regexp = compiled
|
||||
rule.Regexp = regexpStr
|
||||
}
|
||||
|
||||
ok = true
|
||||
|
@ -43,6 +43,7 @@ type MinioStorageConfig struct {
|
||||
Endpoint string `ini:"MINIO_ENDPOINT" json:",omitempty"`
|
||||
AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID" json:",omitempty"`
|
||||
SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY" json:",omitempty"`
|
||||
IamEndpoint string `ini:"MINIO_IAM_ENDPOINT" json:",omitempty"`
|
||||
Bucket string `ini:"MINIO_BUCKET" json:",omitempty"`
|
||||
Location string `ini:"MINIO_LOCATION" json:",omitempty"`
|
||||
BasePath string `ini:"MINIO_BASE_PATH" json:",omitempty"`
|
||||
|
@ -470,6 +470,19 @@ MINIO_BASE_PATH = /prefix
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_IAM_ENDPOINT = 127.0.0.1
|
||||
MINIO_USE_SSL = true
|
||||
MINIO_BASE_PATH = /prefix
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, loadRepoArchiveFrom(cfg))
|
||||
assert.EqualValues(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint)
|
||||
assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL)
|
||||
assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[storage]
|
||||
STORAGE_TYPE = minio
|
||||
MINIO_ACCESS_KEY_ID = my_access_key
|
||||
MINIO_SECRET_ACCESS_KEY = my_secret_key
|
||||
MINIO_USE_SSL = true
|
||||
|
@ -97,7 +97,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
|
||||
}
|
||||
|
||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||
Creds: buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
|
||||
Creds: buildMinioCredentials(config),
|
||||
Secure: config.UseSSL,
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
|
||||
Region: config.Location,
|
||||
@ -164,7 +164,7 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
|
||||
return p
|
||||
}
|
||||
|
||||
func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string) *credentials.Credentials {
|
||||
func buildMinioCredentials(config setting.MinioStorageConfig) *credentials.Credentials {
|
||||
// If static credentials are provided, use those
|
||||
if config.AccessKeyID != "" {
|
||||
return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
|
||||
@ -184,7 +184,9 @@ func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string
|
||||
&credentials.FileAWSCredentials{},
|
||||
// read IAM role from EC2 metadata endpoint if available
|
||||
&credentials.IAM{
|
||||
Endpoint: iamEndpoint,
|
||||
// passing in an empty Endpoint lets the IAM Provider
|
||||
// decide which endpoint to resolve internally
|
||||
Endpoint: config.IamEndpoint,
|
||||
Client: &http.Client{
|
||||
Transport: http.DefaultTransport,
|
||||
},
|
||||
|
@ -107,8 +107,9 @@ func TestMinioCredentials(t *testing.T) {
|
||||
cfg := setting.MinioStorageConfig{
|
||||
AccessKeyID: ExpectedAccessKey,
|
||||
SecretAccessKey: ExpectedSecretAccessKey,
|
||||
IamEndpoint: FakeEndpoint,
|
||||
}
|
||||
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@ -117,13 +118,15 @@ func TestMinioCredentials(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Chain", func(t *testing.T) {
|
||||
cfg := setting.MinioStorageConfig{}
|
||||
cfg := setting.MinioStorageConfig{
|
||||
IamEndpoint: FakeEndpoint,
|
||||
}
|
||||
|
||||
t.Run("EnvMinio", func(t *testing.T) {
|
||||
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
|
||||
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
|
||||
|
||||
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@ -135,7 +138,7 @@ func TestMinioCredentials(t *testing.T) {
|
||||
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
|
||||
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
|
||||
|
||||
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@ -144,11 +147,11 @@ func TestMinioCredentials(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("FileMinio", func(t *testing.T) {
|
||||
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
|
||||
// prevent loading any actual credentials files from the user
|
||||
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
|
||||
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
||||
|
||||
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@ -161,7 +164,7 @@ func TestMinioCredentials(t *testing.T) {
|
||||
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
||||
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
|
||||
|
||||
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||
creds := buildMinioCredentials(cfg)
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@ -187,7 +190,9 @@ func TestMinioCredentials(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
// Use the provided EC2 Instance Metadata server
|
||||
creds := buildMinioCredentials(cfg, server.URL)
|
||||
creds := buildMinioCredentials(setting.MinioStorageConfig{
|
||||
IamEndpoint: server.URL,
|
||||
})
|
||||
v, err := creds.Get()
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
gitea_html "code.gitea.io/gitea/modules/html"
|
||||
gitea_html "code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
)
|
||||
|
@ -10,12 +10,12 @@ import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
@ -39,7 +39,7 @@ func NewFuncMap() template.FuncMap {
|
||||
"Iif": iif,
|
||||
"Eval": evalTokens,
|
||||
"SafeHTML": safeHTML,
|
||||
"HTMLFormat": HTMLFormat,
|
||||
"HTMLFormat": htmlutil.HTMLFormat,
|
||||
"HTMLEscape": htmlEscape,
|
||||
"QueryEscape": queryEscape,
|
||||
"JSEscape": jsEscapeSafe,
|
||||
@ -184,23 +184,6 @@ func NewFuncMap() template.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
func HTMLFormat(s string, rawArgs ...any) template.HTML {
|
||||
args := slices.Clone(rawArgs)
|
||||
for i, v := range args {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
||||
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
||||
case string:
|
||||
args[i] = template.HTMLEscapeString(v)
|
||||
case fmt.Stringer:
|
||||
args[i] = template.HTMLEscapeString(v.String())
|
||||
default:
|
||||
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
return template.HTML(fmt.Sprintf(s, args...))
|
||||
}
|
||||
|
||||
// safeHTML render raw as HTML
|
||||
func safeHTML(s any) template.HTML {
|
||||
switch v := s.(type) {
|
||||
|
@ -61,10 +61,6 @@ func TestJSEscapeSafe(t *testing.T) {
|
||||
assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, jsEscapeSafe(`&<>'"`))
|
||||
}
|
||||
|
||||
func TestHTMLFormat(t *testing.T) {
|
||||
assert.Equal(t, template.HTML("<a>< < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1))
|
||||
}
|
||||
|
||||
func TestSanitizeHTML(t *testing.T) {
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
gitea_html "code.gitea.io/gitea/modules/html"
|
||||
gitea_html "code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
@ -37,10 +38,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string)
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ut.ctx,
|
||||
Metas: metas,
|
||||
}, cleanMsg)
|
||||
fullMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg)
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
@ -67,10 +65,7 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
|
||||
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
||||
Ctx: ut.ctx,
|
||||
Metas: metas,
|
||||
}, urlDefault, template.HTMLEscapeString(msgLine))
|
||||
renderedMessage, err := markup.RenderCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessageSubject: %v", err)
|
||||
return ""
|
||||
@ -92,10 +87,7 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
|
||||
return ""
|
||||
}
|
||||
|
||||
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ut.ctx,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
renderedMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
@ -114,10 +106,7 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
||||
|
||||
// RenderIssueTitle renders issue/pull title with defined post processors
|
||||
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
|
||||
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
||||
Ctx: ut.ctx,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(text))
|
||||
renderedText, err := markup.RenderIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderIssueTitle: %v", err)
|
||||
return ""
|
||||
@ -140,7 +129,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
||||
|
||||
if labelScope == "" {
|
||||
// Regular label
|
||||
return HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`,
|
||||
return htmlutil.HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`,
|
||||
extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name))
|
||||
}
|
||||
|
||||
@ -174,7 +163,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
||||
itemColor := "#" + hex.EncodeToString(itemBytes)
|
||||
scopeColor := "#" + hex.EncodeToString(scopeBytes)
|
||||
|
||||
return HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
|
||||
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
|
||||
`</span>`,
|
||||
@ -185,7 +174,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
|
||||
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ut.ctx}, template.HTMLEscapeString(text))
|
||||
renderedText, err := markup.RenderEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderEmoji: %v", err)
|
||||
return ""
|
||||
@ -207,10 +196,7 @@ func reactionToEmoji(reaction string) template.HTML {
|
||||
}
|
||||
|
||||
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive
|
||||
output, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ut.ctx,
|
||||
Metas: markup.ComposeSimpleDocumentMetas(),
|
||||
}, input)
|
||||
output, err := markdown.RenderString(markup.NewRenderContext(ut.ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), input)
|
||||
if err != nil {
|
||||
log.Error("RenderString: %v", err)
|
||||
}
|
||||
|
@ -113,34 +113,34 @@ func TestRenderCommitBody(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := `/just/a/path.bin
|
||||
<a href="https://example.com/file.bin" class="link">https://example.com/file.bin</a>
|
||||
<a href="https://example.com/file.bin">https://example.com/file.bin</a>
|
||||
[local link](file.bin)
|
||||
[remote link](<a href="https://example.com" class="link">https://example.com</a>)
|
||||
[remote link](<a href="https://example.com">https://example.com</a>)
|
||||
[[local link|file.bin]]
|
||||
[[remote link|<a href="https://example.com" class="link">https://example.com</a>]]
|
||||
[[remote link|<a href="https://example.com">https://example.com</a>]]
|
||||
![local image](image.jpg)
|
||||
![remote image](<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>)
|
||||
![remote image](<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>)
|
||||
[[local image|image.jpg]]
|
||||
[[remote link|<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>]]
|
||||
[[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]]
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||
<span class="emoji" aria-label="thumbs up">👍</span>
|
||||
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
|
||||
<a href="/mention-user" class="mention">@mention-user</a> test
|
||||
<a href="mailto:mail@domain.com">mail@domain.com</a>
|
||||
<a href="/mention-user">@mention-user</a> test
|
||||
<a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
|
||||
space`
|
||||
assert.EqualValues(t, expected, string(newTestRenderUtils().RenderCommitBody(testInput(), testMetas)))
|
||||
}
|
||||
|
||||
func TestRenderCommitMessage(t *testing.T) {
|
||||
expected := `space <a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a> `
|
||||
expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a> `
|
||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessage(testInput(), testMetas))
|
||||
}
|
||||
|
||||
func TestRenderCommitMessageLinkSubject(t *testing.T) {
|
||||
expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="" class="mention">@mention-user</a>`
|
||||
expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>`
|
||||
assert.EqualValues(t, expected, newTestRenderUtils().RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas))
|
||||
}
|
||||
|
||||
|
@ -104,6 +104,7 @@ copy_url = Copy URL
|
||||
copy_hash = Copy hash
|
||||
copy_content = Copy content
|
||||
copy_branch = Copy branch name
|
||||
copy_path = Copy path
|
||||
copy_success = Copied!
|
||||
copy_error = Copy failed
|
||||
copy_type_unsupported = This file type cannot be copied
|
||||
@ -352,6 +353,7 @@ enable_update_checker = Enable Update Checker
|
||||
enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io.
|
||||
env_config_keys = Environment Configuration
|
||||
env_config_keys_prompt = The following environment variables will also be applied to your configuration file:
|
||||
config_write_file_prompt = These configuration options will be written into: %s
|
||||
|
||||
[home]
|
||||
nav_menu = Navigation Menu
|
||||
@ -457,6 +459,7 @@ authorize_application = Authorize Application
|
||||
authorize_redirect_notice = You will be redirected to %s if you authorize this application.
|
||||
authorize_application_created_by = This application was created by %s.
|
||||
authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations.
|
||||
authorize_application_with_scopes = With scopes: %s
|
||||
authorize_title = Authorize "%s" to access your account?
|
||||
authorization_failed = Authorization failed
|
||||
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize.
|
||||
|
@ -99,9 +99,7 @@ func MarkdownRaw(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
defer ctx.Req.Body.Close()
|
||||
if err := markdown.RenderRaw(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
}, ctx.Req.Body, ctx.Resp); err != nil {
|
||||
if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
@ -133,11 +133,6 @@ func DeleteBranch(ctx *context.APIContext) {
|
||||
|
||||
branchName := ctx.PathParam("*")
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.Error(http.StatusForbidden, "", "Git Repository is empty.")
|
||||
return
|
||||
}
|
||||
|
||||
// check whether branches of this repository has been synced
|
||||
totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user