Compare commits
57 commits
3b534855d8
...
7c22dab8a3
Author | SHA1 | Date | |
---|---|---|---|
7c22dab8a3 | |||
d3a962d068 | |||
|
3380217da1 | ||
|
e9aa373db5 | ||
|
1e5940b020 | ||
|
5322136af8 | ||
|
d095e4fdc5 | ||
|
a2b1082dda | ||
|
d7b11f5378 | ||
|
5ef4992fd7 | ||
|
75730a6ded | ||
|
48bcb1937e | ||
|
4903135a93 | ||
|
6f87e71f0c | ||
|
5cc6361e31 | ||
|
0d7893ca8a | ||
|
44f2592028 | ||
|
d2c16d9c2d | ||
|
0b0b506b74 | ||
|
939a66e25c | ||
|
585f74c2ca | ||
|
2af5a75d71 | ||
|
685ebdba63 | ||
|
f59a6cc0e4 | ||
|
e02448bbf5 | ||
|
e291ea5e33 | ||
|
8726ce2635 | ||
|
3ddfca10ac | ||
|
6b4cb070cc | ||
|
c70eb32280 | ||
|
c0ccd4c2d7 | ||
|
f302373eb4 | ||
|
5d18f4b19f | ||
|
d7408d8b0b | ||
|
6dfe993913 | ||
|
1bbc1adcdc | ||
|
d610ea3fbb | ||
|
44df78edd4 | ||
|
1fd3cc3217 | ||
|
f2c3491b61 | ||
|
713652e3d8 | ||
|
b4fb797b32 | ||
|
2a5d5da930 | ||
|
64373004b5 | ||
|
2a321fcfda | ||
|
d6798ae015 | ||
|
cf1174acbf | ||
|
62c33f92a9 | ||
|
f142ae18c0 | ||
|
2e50870688 | ||
|
2716e2f626 | ||
|
e0fe8a8ab4 | ||
|
c50af699ea | ||
|
915c60f8c1 | ||
|
a1e6944bd7 | ||
|
d7e67cf616 | ||
|
ee48c0d5ea |
73 changed files with 975 additions and 281 deletions
|
@ -13,46 +13,42 @@ groups:
|
||||||
-
|
-
|
||||||
name: BREAKING
|
name: BREAKING
|
||||||
labels:
|
labels:
|
||||||
- kind/breaking
|
- pr/breaking
|
||||||
-
|
-
|
||||||
name: SECURITY
|
name: SECURITY
|
||||||
labels:
|
labels:
|
||||||
- kind/security
|
- topic/security
|
||||||
-
|
-
|
||||||
name: FEATURES
|
name: FEATURES
|
||||||
labels:
|
labels:
|
||||||
- kind/feature
|
- type/feature
|
||||||
-
|
-
|
||||||
name: API
|
name: API
|
||||||
labels:
|
labels:
|
||||||
- kind/api
|
- modifies/api
|
||||||
-
|
-
|
||||||
name: ENHANCEMENTS
|
name: ENHANCEMENTS
|
||||||
labels:
|
labels:
|
||||||
- kind/enhancement
|
- type/enhancement
|
||||||
- kind/refactor
|
- type/refactoring
|
||||||
- kind/ui
|
- topic/ui
|
||||||
-
|
-
|
||||||
name: BUGFIXES
|
name: BUGFIXES
|
||||||
labels:
|
labels:
|
||||||
- kind/bug
|
- type/bug
|
||||||
-
|
-
|
||||||
name: TESTING
|
name: TESTING
|
||||||
labels:
|
labels:
|
||||||
- kind/testing
|
- type/testing
|
||||||
-
|
|
||||||
name: TRANSLATION
|
|
||||||
labels:
|
|
||||||
- kind/translation
|
|
||||||
-
|
-
|
||||||
name: BUILD
|
name: BUILD
|
||||||
labels:
|
labels:
|
||||||
- kind/build
|
- topic/build
|
||||||
- kind/lint
|
- topic/code-linting
|
||||||
-
|
-
|
||||||
name: DOCS
|
name: DOCS
|
||||||
labels:
|
labels:
|
||||||
- kind/docs
|
- type/docs
|
||||||
-
|
-
|
||||||
name: MISC
|
name: MISC
|
||||||
default: true
|
default: true
|
||||||
|
|
|
@ -10,6 +10,8 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
lint-backend:
|
lint-backend:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: 'docker.io/node:20-bookworm'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
|
@ -22,6 +24,8 @@ jobs:
|
||||||
TAGS: bindata sqlite sqlite_unlock_notify
|
TAGS: bindata sqlite sqlite_unlock_notify
|
||||||
checks-backend:
|
checks-backend:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: 'docker.io/node:20-bookworm'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
|
@ -34,7 +38,7 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [lint-backend, checks-backend]
|
needs: [lint-backend, checks-backend]
|
||||||
container:
|
container:
|
||||||
image: codeberg.org/forgejo/test_env:1.20
|
image: 'docker.io/node:20-bookworm'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
|
@ -42,15 +46,16 @@ jobs:
|
||||||
go-version: "1.20"
|
go-version: "1.20"
|
||||||
- run: |
|
- run: |
|
||||||
git config --add safe.directory '*'
|
git config --add safe.directory '*'
|
||||||
chown -R gitea:gitea . /go
|
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||||
|
chown -R forgejo:forgejo .
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make deps-backend'
|
su forgejo -c 'make deps-backend'
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make backend'
|
su forgejo -c 'make backend'
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make unit-test-coverage test-check'
|
su forgejo -c 'make unit-test-coverage test-check'
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
RACE_ENABLED: 'true'
|
RACE_ENABLED: 'true'
|
||||||
|
@ -59,7 +64,7 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [lint-backend, checks-backend]
|
needs: [lint-backend, checks-backend]
|
||||||
container:
|
container:
|
||||||
image: codeberg.org/forgejo/test_env:1.20
|
image: 'docker.io/node:20-bookworm'
|
||||||
services:
|
services:
|
||||||
mysql8:
|
mysql8:
|
||||||
image: mysql:8-debian
|
image: mysql:8-debian
|
||||||
|
@ -77,17 +82,24 @@ jobs:
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.20"
|
||||||
- run: |
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install --no-install-recommends -qq -y git-lfs
|
||||||
|
- name: setup user and permissions
|
||||||
|
run: |
|
||||||
git config --add safe.directory '*'
|
git config --add safe.directory '*'
|
||||||
chown -R gitea:gitea . /go
|
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||||
|
chown -R forgejo:forgejo .
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make deps-backend'
|
su forgejo -c 'make deps-backend'
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make backend'
|
su forgejo -c 'make backend'
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make test-mysql8-migration test-mysql8'
|
su forgejo -c 'make test-mysql8-migration test-mysql8'
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
|
@ -96,10 +108,10 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [lint-backend, checks-backend]
|
needs: [lint-backend, checks-backend]
|
||||||
container:
|
container:
|
||||||
image: codeberg.org/forgejo/test_env:1.20
|
image: 'docker.io/node:20-bookworm'
|
||||||
services:
|
services:
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:15
|
image: 'docker.io/postgres:15'
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: test
|
POSTGRES_DB: test
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
@ -110,17 +122,24 @@ jobs:
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.20"
|
||||||
- run: |
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install --no-install-recommends -qq -y git-lfs
|
||||||
|
- name: setup user and permissions
|
||||||
|
run: |
|
||||||
git config --add safe.directory '*'
|
git config --add safe.directory '*'
|
||||||
chown -R gitea:gitea . /go
|
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||||
|
chown -R forgejo:forgejo .
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make deps-backend'
|
su forgejo -c 'make deps-backend'
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make backend'
|
su forgejo -c 'make backend'
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make test-pgsql-migration test-pgsql'
|
su forgejo -c 'make test-pgsql-migration test-pgsql'
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
TAGS: bindata gogit
|
||||||
|
@ -131,23 +150,30 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [lint-backend, checks-backend]
|
needs: [lint-backend, checks-backend]
|
||||||
container:
|
container:
|
||||||
image: codeberg.org/forgejo/test_env:1.20
|
image: 'docker.io/node:20-bookworm'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.20"
|
||||||
- run: |
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install --no-install-recommends -qq -y git-lfs
|
||||||
|
- name: setup user and permissions
|
||||||
|
run: |
|
||||||
git config --add safe.directory '*'
|
git config --add safe.directory '*'
|
||||||
chown -R gitea:gitea . /go
|
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||||
|
chown -R forgejo:forgejo .
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make deps-backend'
|
su forgejo -c 'make deps-backend'
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make backend'
|
su forgejo -c 'make backend'
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
- run: |
|
- run: |
|
||||||
su gitea -c 'make test-sqlite-migration test-sqlite'
|
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
|
|
|
@ -3,7 +3,7 @@ pipeline:
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx
|
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||||
settings:
|
settings:
|
||||||
repo: gitea.nulo.in/nulo/forgejo
|
repo: gitea.nulo.in/nulo/forgejo
|
||||||
tag: v1.20.5-0
|
tag: v1.20.5-1
|
||||||
registry: https://gitea.nulo.in
|
registry: https://gitea.nulo.in
|
||||||
username: Nulo
|
username: Nulo
|
||||||
password:
|
password:
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -4,6 +4,33 @@ This changelog goes through all the changes that have been made in each release
|
||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/1.20.5) - 2023-10-03
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Fix z-index on markdown completion (#27237) (#27242 & #27238)
|
||||||
|
* Use secure cookie for HTTPS sites (#26999) (#27013)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix git 2.11 error when checking IsEmpty (#27393) (#27396)
|
||||||
|
* Allow get release download files and lfs files with oauth2 token format (#26430) (#27378)
|
||||||
|
* Fix orphan check for deleted branch (#27310) (#27320)
|
||||||
|
* Quote table `release` in sql queries (#27205) (#27219)
|
||||||
|
* Fix release URL in webhooks (#27182) (#27184)
|
||||||
|
* Fix successful return value for `SyncAndGetUserSpecificDiff` (#27152) (#27156)
|
||||||
|
* fix pagination for followers and following (#27127) (#27138)
|
||||||
|
* Fix issue templates when blank isses are disabled (#27061) (#27082)
|
||||||
|
* Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017)
|
||||||
|
* Fix INI parsing for value with trailing slash (#26995) (#27001)
|
||||||
|
* Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27249)
|
||||||
|
* Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27167 & #27162)
|
||||||
|
* Fix bug of review request number (#27406) (#27104)
|
||||||
|
* TESTING
|
||||||
|
* services/wiki: Close() after error handling (#27129) (#27137)
|
||||||
|
* DOCS
|
||||||
|
* Improve actions docs related to `pull_request` event (#27126) (#27145)
|
||||||
|
* MISC
|
||||||
|
* Add logs for data broken of comment review (#27326) (#27344)
|
||||||
|
* Load reviewer before sending notification (#27063) (#27064)
|
||||||
|
|
||||||
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
|
## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
||||||
VERSION = ${GITEA_VERSION}
|
VERSION = ${GITEA_VERSION}
|
||||||
|
|
||||||
# SemVer
|
# SemVer
|
||||||
FORGEJO_VERSION := 5.0.5+0-gitea-1.20.5
|
FORGEJO_VERSION := 5.0.6+0-gitea-1.20.5
|
||||||
|
|
||||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,10 @@ type ActionTaskOutput struct {
|
||||||
OutputValue string `xorm:"MEDIUMTEXT"`
|
OutputValue string `xorm:"MEDIUMTEXT"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(ActionTaskOutput))
|
||||||
|
}
|
||||||
|
|
||||||
// FindTaskOutputByTaskID returns the outputs of the task.
|
// FindTaskOutputByTaskID returns the outputs of the task.
|
||||||
func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) {
|
func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) {
|
||||||
var outputs []*ActionTaskOutput
|
var outputs []*ActionTaskOutput
|
||||||
|
|
|
@ -232,7 +232,7 @@ func CreateSource(source *Source) error {
|
||||||
err = registerableSource.RegisterSource()
|
err = registerableSource.RegisterSource()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// remove the AuthSource in case of errors while registering configuration
|
// remove the AuthSource in case of errors while registering configuration
|
||||||
if _, err := db.GetEngine(db.DefaultContext).Delete(source); err != nil {
|
if _, err := db.GetEngine(db.DefaultContext).ID(source.ID).Delete(new(Source)); err != nil {
|
||||||
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
|
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1014,6 +1014,7 @@ type FindCommentsOptions struct {
|
||||||
Type CommentType
|
Type CommentType
|
||||||
IssueIDs []int64
|
IssueIDs []int64
|
||||||
Invalidated util.OptionalBool
|
Invalidated util.OptionalBool
|
||||||
|
IsPull util.OptionalBool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToConds implements FindOptions interface
|
// ToConds implements FindOptions interface
|
||||||
|
@ -1048,6 +1049,9 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
||||||
if !opts.Invalidated.IsNone() {
|
if !opts.Invalidated.IsNone() {
|
||||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
||||||
}
|
}
|
||||||
|
if opts.IsPull != util.OptionalBoolNone {
|
||||||
|
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1055,7 +1059,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
||||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||||
comments := make([]*Comment, 0, 10)
|
comments := make([]*Comment, 0, 10)
|
||||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||||
if opts.RepoID > 0 {
|
if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
|
||||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -637,12 +637,12 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||||
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
|
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cond = cond.Or(
|
|
||||||
// 4. Repositories that we directly own
|
// 4. Repositories that we directly own
|
||||||
builder.Eq{"`repository`.owner_id": user.ID},
|
cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
|
||||||
|
if !user.IsRestricted {
|
||||||
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
||||||
userOrgPublicRepoCond(user.ID),
|
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
|
|
|
@ -7,6 +7,7 @@ package unittest
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -28,6 +29,16 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
|
||||||
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine)
|
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OverrideFixtures(opts FixturesOptions, engine ...*xorm.Engine) func() {
|
||||||
|
old := fixturesLoader
|
||||||
|
if err := InitFixtures(opts, engine...); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
fixturesLoader = old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InitFixtures initialize test fixtures for a test database
|
// InitFixtures initialize test fixtures for a test database
|
||||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||||
e := GetXORMEngine(engine...)
|
e := GetXORMEngine(engine...)
|
||||||
|
@ -37,6 +48,12 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||||
} else {
|
} else {
|
||||||
fixtureOptionFiles = testfixtures.Files(opts.Files...)
|
fixtureOptionFiles = testfixtures.Files(opts.Files...)
|
||||||
}
|
}
|
||||||
|
var fixtureOptionDirs []func(*testfixtures.Loader) error
|
||||||
|
if opts.Dirs != nil {
|
||||||
|
for _, dir := range opts.Dirs {
|
||||||
|
fixtureOptionDirs = append(fixtureOptionDirs, testfixtures.Directory(filepath.Join(opts.Base, dir)))
|
||||||
|
}
|
||||||
|
}
|
||||||
dialect := "unknown"
|
dialect := "unknown"
|
||||||
switch e.Dialect().URI().DBType {
|
switch e.Dialect().URI().DBType {
|
||||||
case schemas.POSTGRES:
|
case schemas.POSTGRES:
|
||||||
|
@ -57,6 +74,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||||
testfixtures.DangerousSkipTestDatabaseCheck(),
|
testfixtures.DangerousSkipTestDatabaseCheck(),
|
||||||
fixtureOptionFiles,
|
fixtureOptionFiles,
|
||||||
}
|
}
|
||||||
|
loaderOptions = append(loaderOptions, fixtureOptionDirs...)
|
||||||
|
|
||||||
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
||||||
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
|
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
|
||||||
|
|
|
@ -198,6 +198,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
|
||||||
type FixturesOptions struct {
|
type FixturesOptions struct {
|
||||||
Dir string
|
Dir string
|
||||||
Files []string
|
Files []string
|
||||||
|
Dirs []string
|
||||||
|
Base string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir
|
// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -197,39 +196,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOtpHeader(header http.Header) string {
|
|
||||||
otpHeader := header.Get("X-Gitea-OTP")
|
|
||||||
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
|
|
||||||
otpHeader = forgejoHeader
|
|
||||||
}
|
|
||||||
return otpHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckForOTP validates OTP
|
|
||||||
func (ctx *APIContext) CheckForOTP() {
|
|
||||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
|
||||||
return // Skip 2FA
|
|
||||||
}
|
|
||||||
|
|
||||||
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
|
|
||||||
if err != nil {
|
|
||||||
if auth.IsErrTwoFactorNotEnrolled(err) {
|
|
||||||
return // No 2FA enrollment for this user
|
|
||||||
}
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok, err := twofa.ValidateTOTP(getOtpHeader(ctx.Req.Header))
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
ctx.Error(http.StatusUnauthorized, "", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIContexter returns apicontext as middleware
|
// APIContexter returns apicontext as middleware
|
||||||
func APIContexter() func(http.Handler) http.Handler {
|
func APIContexter() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetOtpHeader(t *testing.T) {
|
|
||||||
header := http.Header{}
|
|
||||||
assert.EqualValues(t, "", getOtpHeader(header))
|
|
||||||
// Gitea
|
|
||||||
giteaOtp := "123456"
|
|
||||||
header.Set("X-Gitea-OTP", giteaOtp)
|
|
||||||
assert.EqualValues(t, giteaOtp, getOtpHeader(header))
|
|
||||||
// Forgejo has precedence
|
|
||||||
forgejoOtp := "abcdef"
|
|
||||||
header.Set("X-Forgejo-OTP", forgejoOtp)
|
|
||||||
assert.EqualValues(t, forgejoOtp, getOtpHeader(header))
|
|
||||||
}
|
|
|
@ -168,9 +168,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
||||||
// find protected branches without existing repository
|
// find protected branches without existing repository
|
||||||
genericOrphanCheck("Protected Branches without existing repository",
|
genericOrphanCheck("Protected Branches without existing repository",
|
||||||
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
||||||
// find branches without existing repository
|
// find deleted branches without existing repository
|
||||||
genericOrphanCheck("Branches without existing repository",
|
genericOrphanCheck("Deleted Branches without existing repository",
|
||||||
"branch", "repository", "branch.repo_id=repository.id"),
|
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
|
||||||
// find LFS locks without existing repository
|
// find LFS locks without existing repository
|
||||||
genericOrphanCheck("LFS locks without existing repository",
|
genericOrphanCheck("LFS locks without existing repository",
|
||||||
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
||||||
|
|
|
@ -7,12 +7,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
|
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
|
||||||
func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return NewDialContextWithProxy(usage, allowList, blockList, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
// How Go HTTP Client works with redirection:
|
// How Go HTTP Client works with redirection:
|
||||||
// transport.RoundTrip URL=http://domain.com, Host=domain.com
|
// transport.RoundTrip URL=http://domain.com, Host=domain.com
|
||||||
// transport.DialContext addrOrHost=domain.com:80
|
// transport.DialContext addrOrHost=domain.com:80
|
||||||
|
@ -26,11 +31,18 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
|
|
||||||
Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
|
Control: func(network, ipAddr string, c syscall.RawConn) error {
|
||||||
var host string
|
host, port, err := net.SplitHostPort(addrOrHost)
|
||||||
if host, _, err = net.SplitHostPort(addrOrHost); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if proxy != nil {
|
||||||
|
// Always allow the host of the proxy, but only on the specified port.
|
||||||
|
if host == proxy.Hostname() && port == proxy.Port() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
|
// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
|
||||||
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
|
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -66,7 +66,7 @@ var (
|
||||||
// well as the HTML5 spec:
|
// well as the HTML5 spec:
|
||||||
// http://spec.commonmark.org/0.28/#email-address
|
// http://spec.commonmark.org/0.28/#email-address
|
||||||
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
|
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
|
||||||
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|\\.(\\s|$))")
|
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
|
||||||
|
|
||||||
// blackfriday extensions create IDs like fn:user-content-footnote
|
// blackfriday extensions create IDs like fn:user-content-footnote
|
||||||
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
||||||
|
|
|
@ -264,6 +264,18 @@ func TestRender_email(t *testing.T) {
|
||||||
"send email to info@gitea.co.uk.",
|
"send email to info@gitea.co.uk.",
|
||||||
`<p>send email to <a href="mailto:info@gitea.co.uk" rel="nofollow">info@gitea.co.uk</a>.</p>`)
|
`<p>send email to <a href="mailto:info@gitea.co.uk" rel="nofollow">info@gitea.co.uk</a>.</p>`)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`j.doe@example.com,
|
||||||
|
j.doe@example.com.
|
||||||
|
j.doe@example.com;
|
||||||
|
j.doe@example.com?
|
||||||
|
j.doe@example.com!`,
|
||||||
|
`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,<br/>
|
||||||
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.<br/>
|
||||||
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;<br/>
|
||||||
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?<br/>
|
||||||
|
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
|
||||||
|
|
||||||
// Test that should *not* be turned into email links
|
// Test that should *not* be turned into email links
|
||||||
test(
|
test(
|
||||||
"\"info@gitea.com\"",
|
"\"info@gitea.com\"",
|
||||||
|
|
|
@ -16,6 +16,7 @@ type Package struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
|
||||||
|
|
||||||
s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
||||||
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
"<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>"+
|
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||||
"</span>",
|
"</span>",
|
||||||
description,
|
description,
|
||||||
textColor, scopeColor, scopeText,
|
textColor, scopeColor, scopeText,
|
||||||
|
|
|
@ -315,10 +315,6 @@ func reqToken() func(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsBasicAuth {
|
|
||||||
ctx.CheckForOTP()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -340,7 +336,6 @@ func reqBasicAuth() func(ctx *context.APIContext) {
|
||||||
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
|
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.CheckForOTP()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,12 +682,6 @@ func bind[T any](_ T) any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
|
|
||||||
// in the session (if there is a user id stored in session other plugins might return the user
|
|
||||||
// object for that id).
|
|
||||||
//
|
|
||||||
// The Session plugin is expected to be executed second, in order to skip authentication
|
|
||||||
// for users that have already signed in.
|
|
||||||
func buildAuthGroup() *auth.Group {
|
func buildAuthGroup() *auth.Group {
|
||||||
group := auth.NewGroup(
|
group := auth.NewGroup(
|
||||||
&auth.OAuth2{},
|
&auth.OAuth2{},
|
||||||
|
@ -1165,8 +1154,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
m.Group("/issues", func() {
|
m.Group("/issues", func() {
|
||||||
m.Combo("").Get(repo.ListIssues).
|
m.Combo("").Get(repo.ListIssues).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
|
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||||
m.Get("/pinned", repo.ListPinnedIssues)
|
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||||
m.Group("/comments", func() {
|
m.Group("/comments", func() {
|
||||||
m.Get("", repo.ListRepoIssueComments)
|
m.Get("", repo.ListRepoIssueComments)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
|
@ -1308,10 +1297,10 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
|
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
|
||||||
})
|
})
|
||||||
m.Group("/teams", func() {
|
m.Group("/teams", func() {
|
||||||
m.Get("", reqToken(), org.ListTeams)
|
m.Get("", org.ListTeams)
|
||||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||||
m.Get("/search", reqToken(), org.SearchTeam)
|
m.Get("/search", org.SearchTeam)
|
||||||
}, reqOrgMembership())
|
}, reqToken(), reqOrgMembership())
|
||||||
m.Group("/labels", func() {
|
m.Group("/labels", func() {
|
||||||
m.Get("", org.ListLabels)
|
m.Get("", org.ListLabels)
|
||||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
||||||
|
|
|
@ -452,6 +452,24 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
isPull = util.OptionalBoolNone
|
isPull = util.OptionalBoolNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPull != util.OptionalBoolNone && !ctx.Repo.CanWriteIssuesOrPulls(isPull.IsTrue()) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPull == util.OptionalBoolNone {
|
||||||
|
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
|
||||||
|
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||||
|
if !canReadIssues && !canReadPulls {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
} else if !canReadIssues {
|
||||||
|
isPull = util.OptionalBoolTrue
|
||||||
|
} else if !canReadPulls {
|
||||||
|
isPull = util.OptionalBoolFalse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: we should be more efficient here
|
// FIXME: we should be more efficient here
|
||||||
createdByID := getUserIDForFilter(ctx, "created_by")
|
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
|
@ -562,6 +580,10 @@ func GetIssue(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
|
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,11 @@ import (
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
@ -69,6 +71,11 @@ func ListIssueComments(ctx *context.APIContext) {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
|
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
issue.Repo = ctx.Repo.Repository
|
||||||
|
|
||||||
opts := &issues_model.FindCommentsOptions{
|
opts := &issues_model.FindCommentsOptions{
|
||||||
|
@ -265,12 +272,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPull util.OptionalBool
|
||||||
|
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
|
||||||
|
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||||
|
if canReadIssue && canReadPull {
|
||||||
|
isPull = util.OptionalBoolNone
|
||||||
|
} else if canReadIssue {
|
||||||
|
isPull = util.OptionalBoolFalse
|
||||||
|
} else if canReadPull {
|
||||||
|
isPull = util.OptionalBoolTrue
|
||||||
|
} else {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opts := &issues_model.FindCommentsOptions{
|
opts := &issues_model.FindCommentsOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Type: issues_model.CommentTypeComment,
|
Type: issues_model.CommentTypeComment,
|
||||||
Since: since,
|
Since: since,
|
||||||
Before: before,
|
Before: before,
|
||||||
|
IsPull: isPull,
|
||||||
}
|
}
|
||||||
|
|
||||||
comments, err := issues_model.FindComments(ctx, opts)
|
comments, err := issues_model.FindComments(ctx, opts)
|
||||||
|
@ -357,6 +379,11 @@ func CreateIssueComment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
||||||
return
|
return
|
||||||
|
@ -430,6 +457,11 @@ func GetIssueComment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if comment.Type != issues_model.CommentTypeComment {
|
if comment.Type != issues_model.CommentTypeComment {
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
|
@ -548,7 +580,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
if err := comment.LoadIssue(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.Status(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -651,7 +693,17 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
if err := comment.LoadIssue(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.Status(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
} else if comment.Type != issues_model.CommentTypeComment {
|
} else if comment.Type != issues_model.CommentTypeComment {
|
||||||
|
|
|
@ -61,6 +61,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||||
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
if err := comment.LoadIssue(ctx); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
|
@ -186,9 +192,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = comment.LoadIssue(ctx)
|
if err = comment.LoadIssue(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
|
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
|
|
|
@ -155,6 +155,11 @@ func GetDeployKey(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.Status(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = key.GetContent(); err != nil {
|
if err = key.GetContent(); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetContent", err)
|
ctx.Error(http.StatusInternalServerError, "GetContent", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -19,19 +19,19 @@ import (
|
||||||
"code.gitea.io/gitea/modules/web/routing"
|
"code.gitea.io/gitea/modules/web/routing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
|
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
|
||||||
prefix = strings.Trim(prefix, "/")
|
prefix = strings.Trim(prefix, "/")
|
||||||
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
if storageSetting.MinioConfig.ServeDirect {
|
if storageSetting.MinioConfig.ServeDirect {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != "GET" && req.Method != "HEAD" {
|
if req.Method != "GET" && req.Method != "HEAD" {
|
||||||
next.ServeHTTP(w, req)
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||||
next.ServeHTTP(w, req)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
@ -43,7 +43,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||||
http.Error(w, "file not found", http.StatusNotFound)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
||||||
|
@ -57,12 +57,12 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != "GET" && req.Method != "HEAD" {
|
if req.Method != "GET" && req.Method != "HEAD" {
|
||||||
next.ServeHTTP(w, req)
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||||
next.ServeHTTP(w, req)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
@ -70,7 +70,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
||||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||||
rPath = util.PathJoinRelX(rPath)
|
rPath = util.PathJoinRelX(rPath)
|
||||||
if rPath == "" || rPath == "." {
|
if rPath == "" || rPath == "." {
|
||||||
http.Error(w, "file not found", http.StatusNotFound)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||||
http.Error(w, "file not found", http.StatusNotFound)
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||||
|
@ -95,5 +95,4 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
||||||
defer fr.Close()
|
defer fr.Close()
|
||||||
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
|
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
43
routers/web/githttp.go
Normal file
43
routers/web/githttp.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/web/repo"
|
||||||
|
context_service "code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func requireSignIn(ctx *context.Context) {
|
||||||
|
if !setting.Service.RequireSignInView {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// rely on the results of Contexter
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
// TODO: support digit auth - which would be Authorization header with digit
|
||||||
|
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||||
|
ctx.Error(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitHTTPRouters(m *web.Route) {
|
||||||
|
m.Group("", func() {
|
||||||
|
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
|
||||||
|
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
|
||||||
|
m.GetOptions("/info/refs", repo.GetInfoRefs)
|
||||||
|
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
|
||||||
|
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
|
||||||
|
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
|
||||||
|
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
|
||||||
|
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
|
||||||
|
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
|
||||||
|
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
|
||||||
|
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
|
||||||
|
}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
|
||||||
|
}
|
|
@ -251,7 +251,6 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
||||||
isSameRepo = true
|
isSameRepo = true
|
||||||
ci.HeadUser = ctx.Repo.Owner
|
ci.HeadUser = ctx.Repo.Owner
|
||||||
ci.HeadBranch = headInfos[0]
|
ci.HeadBranch = headInfos[0]
|
||||||
|
|
||||||
} else if len(headInfos) == 2 {
|
} else if len(headInfos) == 2 {
|
||||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||||
if len(headInfosSplit) == 1 {
|
if len(headInfosSplit) == 1 {
|
||||||
|
@ -406,6 +405,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer ci.HeadGitRepo.Close()
|
defer ci.HeadGitRepo.Close()
|
||||||
|
} else {
|
||||||
|
ctx.NotFound("ParseCompareInfo", nil)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["HeadRepo"] = ci.HeadRepo
|
ctx.Data["HeadRepo"] = ci.HeadRepo
|
||||||
|
|
|
@ -2971,6 +2971,11 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Error(http.StatusForbidden)
|
ctx.Error(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -3037,6 +3042,11 @@ func DeleteComment(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Error(http.StatusForbidden)
|
ctx.Error(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
@ -3163,6 +3173,11 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
if log.IsTrace() {
|
if log.IsTrace() {
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
|
@ -3306,6 +3321,16 @@ func GetCommentAttachments(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := comment.LoadIssue(ctx); err != nil {
|
||||||
|
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !comment.Type.HasAttachmentSupport() {
|
if !comment.Type.HasAttachmentSupport() {
|
||||||
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
|
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
|
||||||
return
|
return
|
||||||
|
|
|
@ -125,6 +125,10 @@ func GetContentHistoryDetail(ctx *context.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if history.IssueID != issue.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
|
// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
|
||||||
var comment *issues_model.Comment
|
var comment *issues_model.Comment
|
||||||
|
@ -194,11 +198,19 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||||
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if comment.IssueID != issue.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
||||||
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if history.IssueID != issue.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
||||||
if !canSoftDelete {
|
if !canSoftDelete {
|
||||||
|
|
|
@ -89,6 +89,10 @@ func IssuePinMove(ctx *context.Context) {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = issue.MovePin(ctx, form.Position)
|
err = issue.MovePin(ctx, form.Position)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -592,7 +592,17 @@ func DeleteTag(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
||||||
if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil {
|
id := ctx.FormInt64("id")
|
||||||
|
rel, err := repo_model.GetReleaseByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRelease", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Repo.Repository.ID != rel.RepoID {
|
||||||
|
ctx.NotFound("CompareRepoID", repo_model.ErrReleaseNotExist{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := releaseservice.DeleteReleaseByID(ctx, id, ctx.Doer, isDelTag); err != nil {
|
||||||
if models.IsErrProtectedTagName(err) {
|
if models.IsErrProtectedTagName(err) {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -821,6 +821,11 @@ func UsernameSubRoute(ctx *context.Context) {
|
||||||
reloadParam := func(suffix string) (success bool) {
|
reloadParam := func(suffix string) (success bool) {
|
||||||
ctx.SetParams("username", strings.TrimSuffix(username, suffix))
|
ctx.SetParams("username", strings.TrimSuffix(username, suffix))
|
||||||
context_service.UserAssignmentWeb()(ctx)
|
context_service.UserAssignmentWeb()(ctx)
|
||||||
|
// check view permissions
|
||||||
|
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||||
|
ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
return !ctx.Written()
|
return !ctx.Written()
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -422,7 +422,7 @@ func PackageSettingsPost(ctx *context.Context) {
|
||||||
|
|
||||||
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
|
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
|
||||||
// redirect to the package if there are still versions available
|
// redirect to the package if there are still versions available
|
||||||
if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID}); has {
|
if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
|
||||||
redirectURL = ctx.Package.Descriptor.PackageWebLink()
|
redirectURL = ctx.Package.Descriptor.PackageWebLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ignSignInAndCsrf = auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
||||||
|
|
||||||
// registerRoutes register routes
|
// registerRoutes register routes
|
||||||
func registerRoutes(m *web.Route) {
|
func registerRoutes(m *web.Route) {
|
||||||
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
|
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
|
||||||
|
@ -182,7 +184,6 @@ func registerRoutes(m *web.Route) {
|
||||||
// TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView)
|
// TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView)
|
||||||
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
|
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
|
||||||
ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
|
ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
|
||||||
ignSignInAndCsrf := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
|
||||||
validation.AddBindingRules()
|
validation.AddBindingRules()
|
||||||
|
|
||||||
linkAccountEnabled := func(ctx *context.Context) {
|
linkAccountEnabled := func(ctx *context.Context) {
|
||||||
|
@ -1391,19 +1392,7 @@ func registerRoutes(m *web.Route) {
|
||||||
})
|
})
|
||||||
}, ignSignInAndCsrf, lfsServerEnabled)
|
}, ignSignInAndCsrf, lfsServerEnabled)
|
||||||
|
|
||||||
m.Group("", func() {
|
gitHTTPRouters(m)
|
||||||
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
|
|
||||||
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
|
|
||||||
m.GetOptions("/info/refs", repo.GetInfoRefs)
|
|
||||||
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
|
|
||||||
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
|
|
||||||
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
|
|
||||||
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
|
|
||||||
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
|
|
||||||
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
|
|
||||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
|
|
||||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
|
|
||||||
}, ignSignInAndCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// ***** END: Repository *****
|
// ***** END: Repository *****
|
||||||
|
|
|
@ -37,12 +37,16 @@ func isContainerPath(req *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gitRawReleasePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/))`)
|
gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`)
|
||||||
lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
|
lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func isGitRawReleaseOrLFSPath(req *http.Request) bool {
|
func isGitRawOrAttachPath(req *http.Request) bool {
|
||||||
if gitRawReleasePathRe.MatchString(req.URL.Path) {
|
return gitRawOrAttachPathRe.MatchString(req.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
|
||||||
|
if isGitRawOrAttachPath(req) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if setting.LFS.StartServer {
|
if setting.LFS.StartServer {
|
||||||
|
|
|
@ -85,6 +85,10 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||||
"/owner/repo/releases/download/tag/repo.tar.gz",
|
"/owner/repo/releases/download/tag/repo.tar.gz",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955",
|
||||||
|
true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
lfsTests := []string{
|
lfsTests := []string{
|
||||||
"/owner/repo/info/lfs/",
|
"/owner/repo/info/lfs/",
|
||||||
|
@ -104,11 +108,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||||
t.Run(tt.path, func(t *testing.T) {
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
|
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
|
||||||
setting.LFS.StartServer = false
|
setting.LFS.StartServer = false
|
||||||
if got := isGitRawReleaseOrLFSPath(req); got != tt.want {
|
if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
|
||||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
setting.LFS.StartServer = true
|
setting.LFS.StartServer = true
|
||||||
if got := isGitRawReleaseOrLFSPath(req); got != tt.want {
|
if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
|
||||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -117,11 +121,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||||
t.Run(tt, func(t *testing.T) {
|
t.Run(tt, func(t *testing.T) {
|
||||||
req, _ := http.NewRequest("POST", tt, nil)
|
req, _ := http.NewRequest("POST", tt, nil)
|
||||||
setting.LFS.StartServer = false
|
setting.LFS.StartServer = false
|
||||||
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
|
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
|
||||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawReleasePathRe.MatchString(tt))
|
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt))
|
||||||
}
|
}
|
||||||
setting.LFS.StartServer = true
|
setting.LFS.StartServer = true
|
||||||
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
|
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
|
||||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
|
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ func (b *Basic) Name() string {
|
||||||
// Returns nil if header is empty or validation fails.
|
// Returns nil if header is empty or validation fails.
|
||||||
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||||
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
||||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
|
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,11 +133,38 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
|
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||||
store.GetData()["SkipLocalTwoFA"] = true
|
if err := validateTOTP(req, u); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Basic Authorization: Logged in user %-v", u)
|
log.Trace("Basic Authorization: Logged in user %-v", u)
|
||||||
|
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOtpHeader(header http.Header) string {
|
||||||
|
otpHeader := header.Get("X-Gitea-OTP")
|
||||||
|
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
|
||||||
|
otpHeader = forgejoHeader
|
||||||
|
}
|
||||||
|
return otpHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTOTP(req *http.Request, u *user_model.User) error {
|
||||||
|
twofa, err := auth_model.GetTwoFactorByUID(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
if auth_model.IsErrTwoFactorNotEnrolled(err) {
|
||||||
|
// No 2FA enrollment for this user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, err := twofa.ValidateTOTP(getOtpHeader(req.Header)); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
return util.NewInvalidArgumentErrorf("invalid provided OTP")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -216,31 +215,6 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ctx.IsSigned && ctx.IsBasicAuth {
|
|
||||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
|
||||||
return // Skip 2FA
|
|
||||||
}
|
|
||||||
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
|
|
||||||
if err != nil {
|
|
||||||
if auth.IsErrTwoFactorNotEnrolled(err) {
|
|
||||||
return // No 2FA enrollment for this user
|
|
||||||
}
|
|
||||||
ctx.InternalServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
|
||||||
ok, err := twofa.ValidateTOTP(otpHeader)
|
|
||||||
if err != nil {
|
|
||||||
ctx.InternalServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
|
||||||
"message": "Only signed in user is allowed to call APIs.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.AdminRequired {
|
if options.AdminRequired {
|
||||||
|
|
|
@ -128,7 +128,7 @@ func (o *OAuth2) userIDFromToken(tokenSHA string, store DataStore) int64 {
|
||||||
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||||
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
|
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
|
||||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
|
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
|
||||||
!gitRawReleasePathRe.MatchString(req.URL.Path) {
|
!isGitRawOrAttachPath(req) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
|
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
|
||||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
|
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||||
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
|
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
|
||||||
handleSignIn(w, req, sess, user)
|
handleSignIn(w, req, sess, user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
package convert
|
package convert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,12 +13,7 @@ func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
|
||||||
}
|
}
|
||||||
|
|
||||||
func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
||||||
if attach.CustomDownloadURL != "" {
|
return attach.DownloadURL()
|
||||||
return attach.CustomDownloadURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}
|
|
||||||
return setting.AppURL + "api/repos/" + repo.FullName() + "/releases/" + strconv.FormatInt(attach.ReleaseID, 10) + "/assets/" + strconv.FormatInt(attach.ID, 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAttachment converts models.Attachment to api.Attachment for API usage
|
// ToAttachment converts models.Attachment to api.Attachment for API usage
|
||||||
|
|
|
@ -35,6 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
|
||||||
Name: pd.Package.Name,
|
Name: pd.Package.Name,
|
||||||
Version: pd.Version.Version,
|
Version: pd.Version.Version,
|
||||||
CreatedAt: pd.Version.CreatedUnix.AsTime(),
|
CreatedAt: pd.Version.CreatedUnix.AsTime(),
|
||||||
|
HTMLURL: pd.FullWebLink(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,10 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
||||||
oldTitle := issue.Title
|
oldTitle := issue.Title
|
||||||
issue.Title = title
|
issue.Title = title
|
||||||
|
|
||||||
|
if oldTitle == title {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
|
if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -282,6 +282,8 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
||||||
httpClient := NewMigrationHTTPClient()
|
httpClient := NewMigrationHTTPClient()
|
||||||
|
|
||||||
for _, asset := range rel.Attachments {
|
for _, asset := range rel.Attachments {
|
||||||
|
assetID := asset.ID // Don't optimize this, for closure we need a local variable
|
||||||
|
assetDownloadURL := asset.DownloadURL
|
||||||
size := int(asset.Size)
|
size := int(asset.Size)
|
||||||
dlCount := int(asset.DownloadCount)
|
dlCount := int(asset.DownloadCount)
|
||||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||||
|
@ -292,18 +294,18 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
||||||
Created: asset.Created,
|
Created: asset.Created,
|
||||||
DownloadURL: &asset.DownloadURL,
|
DownloadURL: &asset.DownloadURL,
|
||||||
DownloadFunc: func() (io.ReadCloser, error) {
|
DownloadFunc: func() (io.ReadCloser, error) {
|
||||||
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, asset.ID)
|
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, assetID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasBaseURL(asset.DownloadURL, g.baseURL) {
|
if !hasBaseURL(assetDownloadURL, g.baseURL) {
|
||||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
|
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL)
|
||||||
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
|
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: for a private download?
|
// FIXME: for a private download?
|
||||||
req, err := http.NewRequest("GET", asset.DownloadURL, nil)
|
req, err := http.NewRequest("GET", assetDownloadURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,6 +309,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
||||||
httpClient := NewMigrationHTTPClient()
|
httpClient := NewMigrationHTTPClient()
|
||||||
|
|
||||||
for k, asset := range rel.Assets.Links {
|
for k, asset := range rel.Assets.Links {
|
||||||
|
assetID := asset.ID // Don't optimize this, for closure we need a local variable
|
||||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||||
ID: int64(asset.ID),
|
ID: int64(asset.ID),
|
||||||
Name: asset.Name,
|
Name: asset.Name,
|
||||||
|
@ -316,13 +317,13 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
||||||
Size: &zero,
|
Size: &zero,
|
||||||
DownloadCount: &zero,
|
DownloadCount: &zero,
|
||||||
DownloadFunc: func() (io.ReadCloser, error) {
|
DownloadFunc: func() (io.ReadCloser, error) {
|
||||||
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, asset.ID, gitlab.WithContext(g.ctx))
|
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasBaseURL(link.URL, g.baseURL) {
|
if !hasBaseURL(link.URL, g.baseURL) {
|
||||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL)
|
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL)
|
||||||
return io.NopCloser(strings.NewReader(link.URL)), nil
|
return io.NopCloser(strings.NewReader(link.URL)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
|
||||||
if err := pr.LoadIssue(ctx); err != nil {
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
if err := pr.Issue.LoadPoster(ctx); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
|
isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
|
||||||
issueReference := "#"
|
issueReference := "#"
|
||||||
|
|
|
@ -243,7 +243,7 @@ var (
|
||||||
hostMatchers []glob.Glob
|
hostMatchers []glob.Glob
|
||||||
)
|
)
|
||||||
|
|
||||||
func webhookProxy() func(req *http.Request) (*url.URL, error) {
|
func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
|
||||||
if setting.Webhook.ProxyURL == "" {
|
if setting.Webhook.ProxyURL == "" {
|
||||||
return proxy.Proxy()
|
return proxy.Proxy()
|
||||||
}
|
}
|
||||||
|
@ -261,6 +261,9 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
|
||||||
return func(req *http.Request) (*url.URL, error) {
|
return func(req *http.Request) (*url.URL, error) {
|
||||||
for _, v := range hostMatchers {
|
for _, v := range hostMatchers {
|
||||||
if v.Match(req.URL.Host) {
|
if v.Match(req.URL.Host) {
|
||||||
|
if !allowList.MatchHostName(req.URL.Host) {
|
||||||
|
return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
|
||||||
|
}
|
||||||
return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
|
return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,8 +285,8 @@ func Init() error {
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
|
||||||
Proxy: webhookProxy(),
|
Proxy: webhookProxy(allowedHostMatcher),
|
||||||
DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
|
DialContext: hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,35 +14,72 @@ import (
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||||
|
"code.gitea.io/gitea/modules/hostmatcher"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebhookProxy(t *testing.T) {
|
func TestWebhookProxy(t *testing.T) {
|
||||||
|
oldWebhook := setting.Webhook
|
||||||
|
t.Cleanup(func() {
|
||||||
|
setting.Webhook = oldWebhook
|
||||||
|
})
|
||||||
|
|
||||||
setting.Webhook.ProxyURL = "http://localhost:8080"
|
setting.Webhook.ProxyURL = "http://localhost:8080"
|
||||||
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
|
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
|
||||||
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
|
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
|
||||||
|
|
||||||
kases := map[string]string{
|
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
|
||||||
"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
|
|
||||||
"http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080",
|
tests := []struct {
|
||||||
"http://github.com/a/b": "",
|
req string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
|
||||||
|
want: "http://localhost:8080",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: "http://s.discordapp.com/assets/xxxxxx",
|
||||||
|
want: "http://localhost:8080",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: "http://github.com/a/b",
|
||||||
|
want: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
req: "http://www.discordapp.com/assets/xxxxxx",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.req, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("POST", tt.req, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
u, err := webhookProxy(allowedHostMatcher)(req)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for reqURL, proxyURL := range kases {
|
|
||||||
req, err := http.NewRequest("POST", reqURL, nil)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
u, err := webhookProxy()(req)
|
got := ""
|
||||||
assert.NoError(t, err)
|
if u != nil {
|
||||||
if proxyURL == "" {
|
got = u.String()
|
||||||
assert.Nil(t, u)
|
|
||||||
} else {
|
|
||||||
assert.EqualValues(t, proxyURL, u.String())
|
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,6 +173,12 @@ func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
|
||||||
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
|
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||||
|
|
||||||
|
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
||||||
|
}
|
||||||
|
|
||||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
|
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
|
||||||
return &DingtalkPayload{
|
return &DingtalkPayload{
|
||||||
MsgType: "actionCard",
|
MsgType: "actionCard",
|
||||||
|
|
|
@ -256,6 +256,12 @@ func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||||
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
|
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||||
|
|
||||||
|
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetDiscordPayload converts a discord webhook into a DiscordPayload
|
// GetDiscordPayload converts a discord webhook into a DiscordPayload
|
||||||
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||||
s := new(DiscordPayload)
|
s := new(DiscordPayload)
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
type (
|
type (
|
||||||
// FeishuPayload represents
|
// FeishuPayload represents
|
||||||
FeishuPayload struct {
|
FeishuPayload struct {
|
||||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive
|
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
||||||
Content struct {
|
Content struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
|
@ -158,6 +158,12 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||||
return newFeishuTextPayload(text), nil
|
return newFeishuTextPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||||
|
|
||||||
|
return newFeishuTextPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
|
// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
|
||||||
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||||
return convertPayloader(new(FeishuPayload), p, event)
|
return convertPayloader(new(FeishuPayload), p, event)
|
||||||
|
|
|
@ -230,6 +230,24 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
|
||||||
return text, issueTitle, color
|
return text, issueTitle, color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||||
|
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
|
||||||
|
|
||||||
|
switch p.Action {
|
||||||
|
case api.HookPackageCreated:
|
||||||
|
text = fmt.Sprintf("Package created: %s", refLink)
|
||||||
|
color = greenColor
|
||||||
|
case api.HookPackageDeleted:
|
||||||
|
text = fmt.Sprintf("Package deleted: %s", refLink)
|
||||||
|
color = redColor
|
||||||
|
}
|
||||||
|
if withSender {
|
||||||
|
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return text, color
|
||||||
|
}
|
||||||
|
|
||||||
// ToHook convert models.Webhook to api.Hook
|
// ToHook convert models.Webhook to api.Hook
|
||||||
// This function is not part of the convert package to prevent an import cycle
|
// This function is not part of the convert package to prevent an import cycle
|
||||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||||
|
|
|
@ -210,6 +210,21 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
|
||||||
return getMatrixPayload(text, nil, m.MsgType), nil
|
return getMatrixPayload(text, nil, m.MsgType), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
|
||||||
|
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||||
|
var text string
|
||||||
|
|
||||||
|
switch p.Action {
|
||||||
|
case api.HookPackageCreated:
|
||||||
|
text = fmt.Sprintf("[%s] Package published by %s", repoLink, senderLink)
|
||||||
|
case api.HookPackageDeleted:
|
||||||
|
text = fmt.Sprintf("[%s] Package deleted by %s", repoLink, senderLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMatrixPayload(text, nil, m.MsgType), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
|
// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
|
||||||
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||||
s := new(MatrixPayload)
|
s := new(MatrixPayload)
|
||||||
|
|
|
@ -296,6 +296,20 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||||
|
|
||||||
|
return createMSTeamsPayload(
|
||||||
|
p.Repository,
|
||||||
|
p.Sender,
|
||||||
|
title,
|
||||||
|
"",
|
||||||
|
p.Package.HTMLURL,
|
||||||
|
color,
|
||||||
|
&MSTeamsFact{"Package:", p.Package.Name},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
|
// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
|
||||||
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||||
return convertPayloader(new(MSTeamsPayload), p, event)
|
return convertPayloader(new(MSTeamsPayload), p, event)
|
||||||
|
|
|
@ -104,6 +104,10 @@ func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPackagistPayload converts a packagist webhook into a PackagistPayload
|
// GetPackagistPayload converts a packagist webhook into a PackagistPayload
|
||||||
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||||
s := new(PackagistPayload)
|
s := new(PackagistPayload)
|
||||||
|
|
|
@ -22,6 +22,7 @@ type PayloadConvertor interface {
|
||||||
Repository(*api.RepositoryPayload) (api.Payloader, error)
|
Repository(*api.RepositoryPayload) (api.Payloader, error)
|
||||||
Release(*api.ReleasePayload) (api.Payloader, error)
|
Release(*api.ReleasePayload) (api.Payloader, error)
|
||||||
Wiki(*api.WikiPayload) (api.Payloader, error)
|
Wiki(*api.WikiPayload) (api.Payloader, error)
|
||||||
|
Package(*api.PackagePayload) (api.Payloader, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
|
func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
|
||||||
|
@ -53,6 +54,8 @@ func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.
|
||||||
return s.Release(p.(*api.ReleasePayload))
|
return s.Release(p.(*api.ReleasePayload))
|
||||||
case webhook_module.HookEventWiki:
|
case webhook_module.HookEventWiki:
|
||||||
return s.Wiki(p.(*api.WikiPayload))
|
return s.Wiki(p.(*api.WikiPayload))
|
||||||
|
case webhook_module.HookEventPackage:
|
||||||
|
return s.Package(p.(*api.PackagePayload))
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,12 @@ func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||||
return s.createPayload(text, nil), nil
|
return s.createPayload(text, nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
|
||||||
|
|
||||||
|
return s.createPayload(text, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Push implements PayloadConvertor Push method
|
// Push implements PayloadConvertor Push method
|
||||||
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
|
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
|
||||||
// n new commits
|
// n new commits
|
||||||
|
|
|
@ -186,6 +186,12 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
|
||||||
return createTelegramPayload(text), nil
|
return createTelegramPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
|
||||||
|
|
||||||
|
return createTelegramPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetTelegramPayload converts a telegram webhook into a TelegramPayload
|
// GetTelegramPayload converts a telegram webhook into a TelegramPayload
|
||||||
func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||||
return convertPayloader(new(TelegramPayload), p, event)
|
return convertPayloader(new(TelegramPayload), p, event)
|
||||||
|
|
|
@ -179,6 +179,12 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error
|
||||||
return newWechatworkMarkdownPayload(text), nil
|
return newWechatworkMarkdownPayload(text), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||||
|
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||||
|
|
||||||
|
return newWechatworkMarkdownPayload(text), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
|
// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
|
||||||
func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||||
return convertPayloader(new(WechatworkPayload), p, event)
|
return convertPayloader(new(WechatworkPayload), p, event)
|
||||||
|
|
|
@ -325,8 +325,6 @@
|
||||||
<!-- Environment Config -->
|
<!-- Environment Config -->
|
||||||
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
|
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label></label>
|
|
||||||
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
|
|
||||||
<div class="right-content">
|
<div class="right-content">
|
||||||
{{.locale.Tr "install.env_config_keys_prompt"}}
|
{{.locale.Tr "install.env_config_keys_prompt"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
4
templates/swagger/v1_json.tmpl
generated
4
templates/swagger/v1_json.tmpl
generated
|
@ -20249,6 +20249,10 @@
|
||||||
"creator": {
|
"creator": {
|
||||||
"$ref": "#/definitions/User"
|
"$ref": "#/definitions/User"
|
||||||
},
|
},
|
||||||
|
"html_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
|
|
@ -35,6 +35,14 @@ func TestAPIGetCommentAttachment(t *testing.T) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
||||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
session := loginUser(t, repoOwner.Name)
|
session := loginUser(t, repoOwner.Name)
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
|
||||||
|
|
|
@ -174,15 +174,29 @@ func TestAPIGetSystemUserComment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIEditComment(t *testing.T) {
|
func TestAPIEditComment(t *testing.T) {
|
||||||
|
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
const newCommentBody = "This is the new comment body"
|
const newCommentBody = "This is the new comment body"
|
||||||
|
|
||||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1008},
|
||||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||||
|
repoOwner.Name, repo.Name, comment.ID, token)
|
||||||
|
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||||
|
"body": newCommentBody,
|
||||||
|
})
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||||
repoOwner.Name, repo.Name, comment.ID, token)
|
repoOwner.Name, repo.Name, comment.ID, token)
|
||||||
|
@ -199,14 +213,25 @@ func TestAPIEditComment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIDeleteComment(t *testing.T) {
|
func TestAPIDeleteComment(t *testing.T) {
|
||||||
|
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1008},
|
||||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
|
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||||
|
repoOwner.Name, repo.Name, comment.ID, token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||||
repoOwner.Name, repo.Name, comment.ID, token)
|
repoOwner.Name, repo.Name, comment.ID, token)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -107,6 +108,27 @@ func TestAPICommentReactions(t *testing.T) {
|
||||||
})
|
})
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
|
||||||
|
repoOwner.Name, repo.Name, comment.ID, token)
|
||||||
|
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||||
|
Reaction: "+1",
|
||||||
|
})
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||||
|
Reaction: "+1",
|
||||||
|
})
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", urlStr)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
// Add allowed reaction
|
// Add allowed reaction
|
||||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||||
Reaction: "+1",
|
Reaction: "+1",
|
||||||
|
|
|
@ -72,6 +72,17 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
|
||||||
Content: rawKeyBody.Key,
|
Content: rawKeyBody.Key,
|
||||||
Mode: perm.AccessModeRead,
|
Mode: perm.AccessModeRead,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Using the ID of a key that does not belong to the repository must fail
|
||||||
|
{
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d?token=%s", repoOwner.Name, repo.Name, newDeployKey.ID, token))
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
session5 := loginUser(t, "user5")
|
||||||
|
token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user5/repo4/keys/%d?token=%s", newDeployKey.ID, token5))
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateReadWriteDeployKey(t *testing.T) {
|
func TestCreateReadWriteDeployKey(t *testing.T) {
|
||||||
|
|
59
tests/integration/api_twofa_test.go
Normal file
59
tests/integration/api_twofa_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPITwoFactor(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
|
||||||
|
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/user")
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
otpKey, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
SecretSize: 40,
|
||||||
|
Issuer: "gitea-test",
|
||||||
|
AccountName: user.Name,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
tfa := &auth_model.TwoFactor{
|
||||||
|
UID: user.ID,
|
||||||
|
}
|
||||||
|
assert.NoError(t, tfa.SetSecret(otpKey.Secret()))
|
||||||
|
|
||||||
|
assert.NoError(t, auth_model.NewTwoFactor(tfa))
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/user")
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/user")
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
req.Header.Set("X-Gitea-OTP", passcode)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/user")
|
||||||
|
req = AddBasicAuthHeader(req, user.Name)
|
||||||
|
req.Header.Set("X-Forgejo-OTP", passcode)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
9
tests/integration/fixtures/TestAPIComment/comment.yml
Normal file
9
tests/integration/fixtures/TestAPIComment/comment.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1008
|
||||||
|
type: 0 # comment
|
||||||
|
poster_id: 2
|
||||||
|
issue_id: 4 # in repo_id 2
|
||||||
|
content: "comment in private pository"
|
||||||
|
created_unix: 946684811
|
||||||
|
updated_unix: 946684811
|
|
@ -205,6 +205,111 @@ func TestIssueCommentClose(t *testing.T) {
|
||||||
assert.Equal(t, "Description", val)
|
assert.Equal(t, "Description", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentDelete(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||||
|
comment1 := "Test comment 1"
|
||||||
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
||||||
|
|
||||||
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, comment1, comment.Content)
|
||||||
|
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentAttachment(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
const repoURL = "user2/repo1"
|
||||||
|
const content = "Test comment 4"
|
||||||
|
const status = ""
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", issueURL)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
|
||||||
|
assert.True(t, exists, "The template has changed")
|
||||||
|
|
||||||
|
uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK)
|
||||||
|
|
||||||
|
commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||||
|
"_csrf": htmlDoc.GetCSRF(),
|
||||||
|
"content": content,
|
||||||
|
"status": status,
|
||||||
|
"files": uuid,
|
||||||
|
})
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
|
||||||
|
assert.Equal(t, content, val)
|
||||||
|
|
||||||
|
idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
|
||||||
|
idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
|
||||||
|
assert.True(t, has)
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, 0, id)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id))
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id))
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentUpdate(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||||
|
comment1 := "Test comment 1"
|
||||||
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
||||||
|
|
||||||
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, comment1, comment.Content)
|
||||||
|
|
||||||
|
modifiedContent := comment.Content + "MODIFIED"
|
||||||
|
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": modifiedContent,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
commentIdentical := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, comment1, commentIdentical.Content)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": modifiedContent,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, modifiedContent, comment.Content)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIssueReaction(t *testing.T) {
|
func TestIssueReaction(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
|
@ -556,3 +661,45 @@ func TestUpdateIssueDeadline(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
|
assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssuePinMove(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL, issue := testIssueWithBean(t, "user2", 1, "Title", "Content")
|
||||||
|
assert.EqualValues(t, 0, issue.PinOrder)
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/pin", issueURL), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
|
||||||
|
|
||||||
|
position := 1
|
||||||
|
assert.EqualValues(t, position, issue.PinOrder)
|
||||||
|
|
||||||
|
newPosition := 2
|
||||||
|
|
||||||
|
// Using the ID of an issue that does not belong to the repository must fail
|
||||||
|
{
|
||||||
|
session5 := loginUser(t, "user5")
|
||||||
|
movePinURL := "/user5/repo4/issues/move_pin?_csrf=" + GetCSRF(t, session5, issueURL)
|
||||||
|
req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
|
||||||
|
"id": issue.ID,
|
||||||
|
"position": newPosition,
|
||||||
|
})
|
||||||
|
session5.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
|
||||||
|
assert.EqualValues(t, position, issue.PinOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
movePinURL := issueURL[:strings.LastIndexByte(issueURL, '/')] + "/move_pin?_csrf=" + GetCSRF(t, session, issueURL)
|
||||||
|
req = NewRequestWithJSON(t, "POST", movePinURL, map[string]any{
|
||||||
|
"id": issue.ID,
|
||||||
|
"position": newPosition,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
|
||||||
|
assert.EqualValues(t, newPosition, issue.PinOrder)
|
||||||
|
}
|
||||||
|
|
|
@ -89,6 +89,44 @@ func TestCreateRelease(t *testing.T) {
|
||||||
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
|
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteRelease(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 57, OwnerName: "user2", LowerName: "repo-release"})
|
||||||
|
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{TagName: "v2.0"})
|
||||||
|
assert.False(t, release.IsTag)
|
||||||
|
|
||||||
|
// Using the ID of a comment that does not belong to the repository must fail
|
||||||
|
session5 := loginUser(t, "user5")
|
||||||
|
otherRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user5", LowerName: "repo4"})
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session5, otherRepo.Link()),
|
||||||
|
})
|
||||||
|
session5.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/releases/delete?id=%d", repo.Link(), release.ID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, repo.Link()),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
release = unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: release.ID})
|
||||||
|
|
||||||
|
if assert.True(t, release.IsTag) {
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", otherRepo.Link(), release.ID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session5, otherRepo.Link()),
|
||||||
|
})
|
||||||
|
session5.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/tags/delete?id=%d", repo.Link(), release.ID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, repo.Link()),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
unittest.AssertNotExistsBean(t, &repo_model.Release{ID: release.ID})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateReleasePreRelease(t *testing.T) {
|
func TestCreateReleasePreRelease(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
|
|
@ -264,3 +264,13 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
||||||
func Printf(format string, args ...any) {
|
func Printf(format string, args ...any) {
|
||||||
testlogger.Printf(format, args...)
|
testlogger.Printf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddFixtures(dirs ...string) func() {
|
||||||
|
return unittest.OverrideFixtures(
|
||||||
|
unittest.FixturesOptions{
|
||||||
|
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
|
||||||
|
Base: filepath.Dir(setting.AppPath),
|
||||||
|
Dirs: dirs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,11 @@ import {displayError} from './common.js';
|
||||||
|
|
||||||
const {mermaidMaxSourceCharacters} = window.config;
|
const {mermaidMaxSourceCharacters} = window.config;
|
||||||
|
|
||||||
|
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
|
||||||
const iframeCss = `:root {color-scheme: normal}
|
const iframeCss = `:root {color-scheme: normal}
|
||||||
body {margin: 0; padding: 0; overflow: hidden}
|
body {margin: 0; padding: 0; overflow: hidden}
|
||||||
#mermaid {display: block; margin: 0 auto}`;
|
#mermaid {display: block; margin: 0 auto}
|
||||||
|
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
||||||
|
|
||||||
export async function renderMermaid() {
|
export async function renderMermaid() {
|
||||||
const els = document.querySelectorAll('.markup code.language-mermaid');
|
const els = document.querySelectorAll('.markup code.language-mermaid');
|
||||||
|
|
Loading…
Reference in a new issue