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
|
||||
labels:
|
||||
- kind/breaking
|
||||
- pr/breaking
|
||||
-
|
||||
name: SECURITY
|
||||
labels:
|
||||
- kind/security
|
||||
- topic/security
|
||||
-
|
||||
name: FEATURES
|
||||
labels:
|
||||
- kind/feature
|
||||
- type/feature
|
||||
-
|
||||
name: API
|
||||
labels:
|
||||
- kind/api
|
||||
- modifies/api
|
||||
-
|
||||
name: ENHANCEMENTS
|
||||
labels:
|
||||
- kind/enhancement
|
||||
- kind/refactor
|
||||
- kind/ui
|
||||
- type/enhancement
|
||||
- type/refactoring
|
||||
- topic/ui
|
||||
-
|
||||
name: BUGFIXES
|
||||
labels:
|
||||
- kind/bug
|
||||
- type/bug
|
||||
-
|
||||
name: TESTING
|
||||
labels:
|
||||
- kind/testing
|
||||
-
|
||||
name: TRANSLATION
|
||||
labels:
|
||||
- kind/translation
|
||||
- type/testing
|
||||
-
|
||||
name: BUILD
|
||||
labels:
|
||||
- kind/build
|
||||
- kind/lint
|
||||
- topic/build
|
||||
- topic/code-linting
|
||||
-
|
||||
name: DOCS
|
||||
labels:
|
||||
- kind/docs
|
||||
- type/docs
|
||||
-
|
||||
name: MISC
|
||||
default: true
|
||||
|
|
|
@ -10,6 +10,8 @@ on:
|
|||
jobs:
|
||||
lint-backend:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
|
@ -22,6 +24,8 @@ jobs:
|
|||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
checks-backend:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
|
@ -34,7 +38,7 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [lint-backend, checks-backend]
|
||||
container:
|
||||
image: codeberg.org/forgejo/test_env:1.20
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
|
@ -42,15 +46,16 @@ jobs:
|
|||
go-version: "1.20"
|
||||
- run: |
|
||||
git config --add safe.directory '*'
|
||||
chown -R gitea:gitea . /go
|
||||
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||
chown -R forgejo:forgejo .
|
||||
- run: |
|
||||
su gitea -c 'make deps-backend'
|
||||
su forgejo -c 'make deps-backend'
|
||||
- run: |
|
||||
su gitea -c 'make backend'
|
||||
su forgejo -c 'make backend'
|
||||
env:
|
||||
TAGS: bindata
|
||||
- run: |
|
||||
su gitea -c 'make unit-test-coverage test-check'
|
||||
su forgejo -c 'make unit-test-coverage test-check'
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
RACE_ENABLED: 'true'
|
||||
|
@ -59,7 +64,7 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [lint-backend, checks-backend]
|
||||
container:
|
||||
image: codeberg.org/forgejo/test_env:1.20
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
services:
|
||||
mysql8:
|
||||
image: mysql:8-debian
|
||||
|
@ -77,17 +82,24 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
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 '*'
|
||||
chown -R gitea:gitea . /go
|
||||
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||
chown -R forgejo:forgejo .
|
||||
- run: |
|
||||
su gitea -c 'make deps-backend'
|
||||
su forgejo -c 'make deps-backend'
|
||||
- run: |
|
||||
su gitea -c 'make backend'
|
||||
su forgejo -c 'make backend'
|
||||
env:
|
||||
TAGS: bindata
|
||||
- run: |
|
||||
su gitea -c 'make test-mysql8-migration test-mysql8'
|
||||
su forgejo -c 'make test-mysql8-migration test-mysql8'
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata
|
||||
|
@ -96,10 +108,10 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [lint-backend, checks-backend]
|
||||
container:
|
||||
image: codeberg.org/forgejo/test_env:1.20
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:15
|
||||
image: 'docker.io/postgres:15'
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
@ -110,17 +122,24 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
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 '*'
|
||||
chown -R gitea:gitea . /go
|
||||
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||
chown -R forgejo:forgejo .
|
||||
- run: |
|
||||
su gitea -c 'make deps-backend'
|
||||
su forgejo -c 'make deps-backend'
|
||||
- run: |
|
||||
su gitea -c 'make backend'
|
||||
su forgejo -c 'make backend'
|
||||
env:
|
||||
TAGS: bindata
|
||||
- run: |
|
||||
su gitea -c 'make test-pgsql-migration test-pgsql'
|
||||
su forgejo -c 'make test-pgsql-migration test-pgsql'
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
|
@ -131,23 +150,30 @@ jobs:
|
|||
runs-on: docker
|
||||
needs: [lint-backend, checks-backend]
|
||||
container:
|
||||
image: codeberg.org/forgejo/test_env:1.20
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
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 '*'
|
||||
chown -R gitea:gitea . /go
|
||||
adduser --quiet --comment forgejo --disabled-password forgejo
|
||||
chown -R forgejo:forgejo .
|
||||
- run: |
|
||||
su gitea -c 'make deps-backend'
|
||||
su forgejo -c 'make deps-backend'
|
||||
- run: |
|
||||
su gitea -c 'make backend'
|
||||
su forgejo -c 'make backend'
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
- run: |
|
||||
su gitea -c 'make test-sqlite-migration test-sqlite'
|
||||
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
|
|
|
@ -3,7 +3,7 @@ pipeline:
|
|||
image: docker.io/woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
repo: gitea.nulo.in/nulo/forgejo
|
||||
tag: v1.20.5-0
|
||||
tag: v1.20.5-1
|
||||
registry: https://gitea.nulo.in
|
||||
username: Nulo
|
||||
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
|
||||
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
|
||||
|
||||
* SECURITY
|
||||
|
|
2
Makefile
2
Makefile
|
@ -89,7 +89,7 @@ endif
|
|||
VERSION = ${GITEA_VERSION}
|
||||
|
||||
# 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)"
|
||||
|
||||
|
|
|
@ -20,6 +20,10 @@ type ActionTaskOutput struct {
|
|||
OutputValue string `xorm:"MEDIUMTEXT"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionTaskOutput))
|
||||
}
|
||||
|
||||
// FindTaskOutputByTaskID returns the outputs of the task.
|
||||
func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) {
|
||||
var outputs []*ActionTaskOutput
|
||||
|
|
|
@ -232,7 +232,7 @@ func CreateSource(source *Source) error {
|
|||
err = registerableSource.RegisterSource()
|
||||
if err != nil {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1014,6 +1014,7 @@ type FindCommentsOptions struct {
|
|||
Type CommentType
|
||||
IssueIDs []int64
|
||||
Invalidated util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
}
|
||||
|
||||
// ToConds implements FindOptions interface
|
||||
|
@ -1048,6 +1049,9 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
|||
if !opts.Invalidated.IsNone() {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1055,7 +1059,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
|||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
@ -637,12 +637,12 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
|||
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
|
||||
)
|
||||
}
|
||||
cond = cond.Or(
|
||||
// 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
|
||||
userOrgPublicRepoCond(user.ID),
|
||||
)
|
||||
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return cond
|
||||
|
|
|
@ -7,6 +7,7 @@ package unittest
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
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
|
||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
e := GetXORMEngine(engine...)
|
||||
|
@ -37,6 +48,12 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
|||
} else {
|
||||
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"
|
||||
switch e.Dialect().URI().DBType {
|
||||
case schemas.POSTGRES:
|
||||
|
@ -57,6 +74,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
|||
testfixtures.DangerousSkipTestDatabaseCheck(),
|
||||
fixtureOptionFiles,
|
||||
}
|
||||
loaderOptions = append(loaderOptions, fixtureOptionDirs...)
|
||||
|
||||
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
||||
loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
|
||||
|
|
|
@ -198,6 +198,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
|
|||
type FixturesOptions struct {
|
||||
Dir string
|
||||
Files []string
|
||||
Dirs []string
|
||||
Base string
|
||||
}
|
||||
|
||||
// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
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
|
||||
func APIContexter() func(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
|
||||
genericOrphanCheck("Protected Branches without existing repository",
|
||||
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
||||
// find branches without existing repository
|
||||
genericOrphanCheck("Branches without existing repository",
|
||||
"branch", "repository", "branch.repo_id=repository.id"),
|
||||
// find deleted branches without existing repository
|
||||
genericOrphanCheck("Deleted Branches without existing repository",
|
||||
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
|
||||
// find LFS locks without existing repository
|
||||
genericOrphanCheck("LFS locks without existing repository",
|
||||
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
||||
|
|
|
@ -7,12 +7,17 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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:
|
||||
// transport.RoundTrip URL=http://domain.com, Host=domain.com
|
||||
// transport.DialContext addrOrHost=domain.com:80
|
||||
|
@ -26,11 +31,18 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx
|
|||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
|
||||
Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
|
||||
var host string
|
||||
if host, _, err = net.SplitHostPort(addrOrHost); err != nil {
|
||||
Control: func(network, ipAddr string, c syscall.RawConn) error {
|
||||
host, port, err := net.SplitHostPort(addrOrHost)
|
||||
if err != nil {
|
||||
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
|
||||
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
|
||||
if err != nil {
|
||||
|
|
|
@ -66,7 +66,7 @@ var (
|
|||
// well as the HTML5 spec:
|
||||
// http://spec.commonmark.org/0.28/#email-address
|
||||
// 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
|
||||
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
||||
|
|
|
@ -264,6 +264,18 @@ func TestRender_email(t *testing.T) {
|
|||
"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>`)
|
||||
|
||||
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(
|
||||
"\"info@gitea.com\"",
|
||||
|
|
|
@ -16,6 +16,7 @@ type Package struct {
|
|||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
// swagger:strfmt date-time
|
||||
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'>"+
|
||||
"<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>",
|
||||
description,
|
||||
textColor, scopeColor, scopeText,
|
||||
|
|
|
@ -315,10 +315,6 @@ func reqToken() func(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if ctx.IsBasicAuth {
|
||||
ctx.CheckForOTP()
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
return
|
||||
}
|
||||
|
@ -340,7 +336,6 @@ func reqBasicAuth() func(ctx *context.APIContext) {
|
|||
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
|
||||
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 {
|
||||
group := auth.NewGroup(
|
||||
&auth.OAuth2{},
|
||||
|
@ -1165,8 +1154,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Group("/issues", func() {
|
||||
m.Combo("").Get(repo.ListIssues).
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
|
||||
m.Get("/pinned", repo.ListPinnedIssues)
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||
m.Group("/comments", func() {
|
||||
m.Get("", repo.ListRepoIssueComments)
|
||||
m.Group("/{id}", func() {
|
||||
|
@ -1308,10 +1297,10 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
|
||||
})
|
||||
m.Group("/teams", func() {
|
||||
m.Get("", reqToken(), org.ListTeams)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", reqToken(), org.SearchTeam)
|
||||
}, reqOrgMembership())
|
||||
m.Get("", org.ListTeams)
|
||||
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", org.SearchTeam)
|
||||
}, reqToken(), reqOrgMembership())
|
||||
m.Group("/labels", func() {
|
||||
m.Get("", org.ListLabels)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
||||
|
|
|
@ -452,6 +452,24 @@ func ListIssues(ctx *context.APIContext) {
|
|||
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
|
||||
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||
if ctx.Written() {
|
||||
|
@ -562,6 +580,10 @@ func GetIssue(ctx *context.APIContext) {
|
|||
}
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ import (
|
|||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
@ -69,6 +71,11 @@ func ListIssueComments(ctx *context.APIContext) {
|
|||
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
|
@ -265,12 +272,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
|||
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{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Since: since,
|
||||
Before: before,
|
||||
IsPull: isPull,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
|
@ -357,6 +379,11 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
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")))
|
||||
return
|
||||
|
@ -430,6 +457,11 @@ func GetIssueComment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Type != issues_model.CommentTypeComment {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
|
@ -548,7 +580,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
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)
|
||||
return
|
||||
}
|
||||
|
@ -651,7 +693,17 @@ func deleteIssueComment(ctx *context.APIContext) {
|
|||
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)
|
||||
return
|
||||
} else if comment.Type != issues_model.CommentTypeComment {
|
||||
|
|
|
@ -61,6 +61,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
|||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
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) {
|
||||
|
@ -186,9 +192,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
|||
return
|
||||
}
|
||||
|
||||
err = comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
if err = comment.LoadIssue(ctx); err != nil {
|
||||
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) {
|
||||
|
|
|
@ -155,6 +155,11 @@ func GetDeployKey(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if key.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err = key.GetContent(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetContent", err)
|
||||
return
|
||||
|
|
|
@ -19,19 +19,19 @@ import (
|
|||
"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, "/")
|
||||
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
||||
if storageSetting.MinioConfig.ServeDirect {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
|
@ -43,7 +43,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
|||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
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
|
||||
}
|
||||
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) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
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 = util.PathJoinRelX(rPath)
|
||||
if rPath == "" || rPath == "." {
|
||||
http.Error(w, "file not found", http.StatusNotFound)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
|
|||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
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
|
||||
}
|
||||
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()
|
||||
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
|
||||
ci.HeadUser = ctx.Repo.Owner
|
||||
ci.HeadBranch = headInfos[0]
|
||||
|
||||
} else if len(headInfos) == 2 {
|
||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||
if len(headInfosSplit) == 1 {
|
||||
|
@ -406,6 +405,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
|||
return nil
|
||||
}
|
||||
defer ci.HeadGitRepo.Close()
|
||||
} else {
|
||||
ctx.NotFound("ParseCompareInfo", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["HeadRepo"] = ci.HeadRepo
|
||||
|
|
|
@ -2971,6 +2971,11 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
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)) {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
|
@ -3037,6 +3042,11 @@ func DeleteComment(ctx *context.Context) {
|
|||
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)) {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
|
@ -3163,6 +3173,11 @@ func ChangeCommentReaction(ctx *context.Context) {
|
|||
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 log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
|
@ -3306,6 +3321,16 @@ func GetCommentAttachments(ctx *context.Context) {
|
|||
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() {
|
||||
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
|
||||
return
|
||||
|
|
|
@ -125,6 +125,10 @@ func GetContentHistoryDetail(ctx *context.Context) {
|
|||
})
|
||||
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.
|
||||
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)
|
||||
return
|
||||
}
|
||||
if comment.IssueID != issue.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
}
|
||||
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
||||
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
||||
return
|
||||
}
|
||||
if history.IssueID != issue.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
||||
if !canSoftDelete {
|
||||
|
|
|
@ -89,6 +89,10 @@ func IssuePinMove(ctx *context.Context) {
|
|||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
if issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
err = issue.MovePin(ctx, form.Position)
|
||||
if err != nil {
|
||||
|
|
|
@ -592,7 +592,17 @@ func DeleteTag(ctx *context.Context) {
|
|||
}
|
||||
|
||||
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) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||
} else {
|
||||
|
|
|
@ -821,6 +821,11 @@ func UsernameSubRoute(ctx *context.Context) {
|
|||
reloadParam := func(suffix string) (success bool) {
|
||||
ctx.SetParams("username", strings.TrimSuffix(username, suffix))
|
||||
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()
|
||||
}
|
||||
switch {
|
||||
|
|
|
@ -422,7 +422,7 @@ func PackageSettingsPost(ctx *context.Context) {
|
|||
|
||||
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
|
||||
// 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -175,6 +175,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
return routes
|
||||
}
|
||||
|
||||
var ignSignInAndCsrf = auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
||||
|
||||
// registerRoutes register routes
|
||||
func registerRoutes(m *web.Route) {
|
||||
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)
|
||||
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})
|
||||
ignSignInAndCsrf := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
||||
validation.AddBindingRules()
|
||||
|
||||
linkAccountEnabled := func(ctx *context.Context) {
|
||||
|
@ -1391,19 +1392,7 @@ func registerRoutes(m *web.Route) {
|
|||
})
|
||||
}, ignSignInAndCsrf, lfsServerEnabled)
|
||||
|
||||
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, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
|
||||
gitHTTPRouters(m)
|
||||
})
|
||||
})
|
||||
// ***** END: Repository *****
|
||||
|
|
|
@ -37,12 +37,16 @@ func isContainerPath(req *http.Request) bool {
|
|||
}
|
||||
|
||||
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/`)
|
||||
)
|
||||
|
||||
func isGitRawReleaseOrLFSPath(req *http.Request) bool {
|
||||
if gitRawReleasePathRe.MatchString(req.URL.Path) {
|
||||
func isGitRawOrAttachPath(req *http.Request) bool {
|
||||
return gitRawOrAttachPathRe.MatchString(req.URL.Path)
|
||||
}
|
||||
|
||||
func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
|
||||
if isGitRawOrAttachPath(req) {
|
||||
return true
|
||||
}
|
||||
if setting.LFS.StartServer {
|
||||
|
|
|
@ -85,6 +85,10 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
|||
"/owner/repo/releases/download/tag/repo.tar.gz",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955",
|
||||
true,
|
||||
},
|
||||
}
|
||||
lfsTests := []string{
|
||||
"/owner/repo/info/lfs/",
|
||||
|
@ -104,11 +108,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
|||
t.Run(tt.path, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
@ -117,11 +121,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
|||
t.Run(tt, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", tt, nil)
|
||||
setting.LFS.StartServer = false
|
||||
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawReleasePathRe.MatchString(tt))
|
||||
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt))
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"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.
|
||||
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
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -132,11 +133,38 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
|
||||
store.GetData()["SkipLocalTwoFA"] = true
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Basic Authorization: Logged in user %-v", u)
|
||||
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -216,31 +215,6 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
|
|||
})
|
||||
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 {
|
||||
|
|
|
@ -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) {
|
||||
// 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) &&
|
||||
!gitRawReleasePathRe.MatchString(req.URL.Path) {
|
||||
!isGitRawOrAttachPath(req) {
|
||||
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
|
||||
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) {
|
||||
handleSignIn(w, req, sess, user)
|
||||
}
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
package convert
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
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 {
|
||||
if attach.CustomDownloadURL != "" {
|
||||
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)
|
||||
return attach.DownloadURL()
|
||||
}
|
||||
|
||||
// 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,
|
||||
Version: pd.Version.Version,
|
||||
CreatedAt: pd.Version.CreatedUnix.AsTime(),
|
||||
HTMLURL: pd.FullWebLink(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,10 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
|||
oldTitle := issue.Title
|
||||
issue.Title = title
|
||||
|
||||
if oldTitle == title {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -282,6 +282,8 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
|||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
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)
|
||||
dlCount := int(asset.DownloadCount)
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
|
@ -292,18 +294,18 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
|||
Created: asset.Created,
|
||||
DownloadURL: &asset.DownloadURL,
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hasBaseURL(asset.DownloadURL, g.baseURL) {
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
|
||||
if !hasBaseURL(assetDownloadURL, g.baseURL) {
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL)
|
||||
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
|
||||
}
|
||||
|
||||
// FIXME: for a private download?
|
||||
req, err := http.NewRequest("GET", asset.DownloadURL, nil)
|
||||
req, err := http.NewRequest("GET", assetDownloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -309,6 +309,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
|||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
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{
|
||||
ID: int64(asset.ID),
|
||||
Name: asset.Name,
|
||||
|
@ -316,13 +317,13 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
|||
Size: &zero,
|
||||
DownloadCount: &zero,
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
|
|||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := pr.Issue.LoadPoster(ctx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
|
||||
issueReference := "#"
|
||||
|
|
|
@ -243,7 +243,7 @@ var (
|
|||
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 == "" {
|
||||
return proxy.Proxy()
|
||||
}
|
||||
|
@ -261,6 +261,9 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
|
|||
return func(req *http.Request) (*url.URL, error) {
|
||||
for _, v := range hostMatchers {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -282,8 +285,8 @@ func Init() error {
|
|||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
|
||||
Proxy: webhookProxy(),
|
||||
DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
|
||||
Proxy: webhookProxy(allowedHostMatcher),
|
||||
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/unittest"
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/hostmatcher"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebhookProxy(t *testing.T) {
|
||||
oldWebhook := setting.Webhook
|
||||
t.Cleanup(func() {
|
||||
setting.Webhook = oldWebhook
|
||||
})
|
||||
|
||||
setting.Webhook.ProxyURL = "http://localhost:8080"
|
||||
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
|
||||
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
|
||||
|
||||
kases := map[string]string{
|
||||
"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
|
||||
"http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080",
|
||||
"http://github.com/a/b": "",
|
||||
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
|
||||
|
||||
tests := []struct {
|
||||
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)
|
||||
|
||||
u, err := webhookProxy()(req)
|
||||
assert.NoError(t, err)
|
||||
if proxyURL == "" {
|
||||
assert.Nil(t, u)
|
||||
} else {
|
||||
assert.EqualValues(t, proxyURL, u.String())
|
||||
got := ""
|
||||
if u != nil {
|
||||
got = 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
|
||||
}
|
||||
|
||||
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 {
|
||||
return &DingtalkPayload{
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(DiscordPayload)
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
type (
|
||||
// FeishuPayload represents
|
||||
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 {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
|
@ -158,6 +158,12 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
|||
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
|
||||
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(FeishuPayload), p, event)
|
||||
|
|
|
@ -230,6 +230,24 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
|
|||
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
|
||||
// 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) {
|
||||
|
|
|
@ -210,6 +210,21 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
|
|||
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
|
||||
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(MatrixPayload)
|
||||
|
|
|
@ -296,6 +296,20 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
|||
), 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
|
||||
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(MSTeamsPayload), p, event)
|
||||
|
|
|
@ -104,6 +104,10 @@ func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetPackagistPayload converts a packagist webhook into a PackagistPayload
|
||||
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(PackagistPayload)
|
||||
|
|
|
@ -22,6 +22,7 @@ type PayloadConvertor interface {
|
|||
Repository(*api.RepositoryPayload) (api.Payloader, error)
|
||||
Release(*api.ReleasePayload) (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) {
|
||||
|
@ -53,6 +54,8 @@ func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.
|
|||
return s.Release(p.(*api.ReleasePayload))
|
||||
case webhook_module.HookEventWiki:
|
||||
return s.Wiki(p.(*api.WikiPayload))
|
||||
case webhook_module.HookEventPackage:
|
||||
return s.Package(p.(*api.PackagePayload))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
|
|
@ -171,6 +171,12 @@ func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
|||
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
|
||||
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
|
||||
// n new commits
|
||||
|
|
|
@ -186,6 +186,12 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
|
|||
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
|
||||
func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(TelegramPayload), p, event)
|
||||
|
|
|
@ -179,6 +179,12 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error
|
|||
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
|
||||
func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(WechatworkPayload), p, event)
|
||||
|
|
|
@ -325,8 +325,6 @@
|
|||
<!-- Environment Config -->
|
||||
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
|
||||
<div class="inline field">
|
||||
<label></label>
|
||||
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
|
||||
<div class="right-content">
|
||||
{{.locale.Tr "install.env_config_keys_prompt"}}
|
||||
</div>
|
||||
|
|
4
templates/swagger/v1_json.tmpl
generated
4
templates/swagger/v1_json.tmpl
generated
|
@ -20249,6 +20249,10 @@
|
|||
"creator": {
|
||||
"$ref": "#/definitions/User"
|
||||
},
|
||||
"html_url": {
|
||||
"type": "string",
|
||||
"x-go-name": "HTMLURL"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
|
|
|
@ -35,6 +35,14 @@ func TestAPIGetCommentAttachment(t *testing.T) {
|
|||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -174,15 +174,29 @@ func TestAPIGetSystemUserComment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAPIEditComment(t *testing.T) {
|
||||
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
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))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
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)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
|
@ -199,14 +213,25 @@ func TestAPIEditComment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAPIDeleteComment(t *testing.T) {
|
||||
defer tests.AddFixtures("tests/integration/fixtures/TestAPIComment/")()
|
||||
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))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
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)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -107,6 +108,27 @@ func TestAPICommentReactions(t *testing.T) {
|
|||
})
|
||||
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
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
|
|
|
@ -72,6 +72,17 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
|
|||
Content: rawKeyBody.Key,
|
||||
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) {
|
||||
|
|
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
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"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
@ -264,3 +264,13 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
|||
func Printf(format string, args ...any) {
|
||||
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;
|
||||
|
||||
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
|
||||
const iframeCss = `:root {color-scheme: normal}
|
||||
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() {
|
||||
const els = document.querySelectorAll('.markup code.language-mermaid');
|
||||
|
|
Loading…
Reference in a new issue