Compare commits

..

2 commits

Author SHA1 Message Date
3b534855d8 nulo: woodpecker CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-06 17:36:57 -03:00
136b8c7e0d Dockerfile: rename user to _gitea instead of git 2023-10-06 17:15:49 -03:00
73 changed files with 281 additions and 975 deletions

View file

@ -13,42 +13,46 @@ groups:
- -
name: BREAKING name: BREAKING
labels: labels:
- pr/breaking - kind/breaking
- -
name: SECURITY name: SECURITY
labels: labels:
- topic/security - kind/security
- -
name: FEATURES name: FEATURES
labels: labels:
- type/feature - kind/feature
- -
name: API name: API
labels: labels:
- modifies/api - kind/api
- -
name: ENHANCEMENTS name: ENHANCEMENTS
labels: labels:
- type/enhancement - kind/enhancement
- type/refactoring - kind/refactor
- topic/ui - kind/ui
- -
name: BUGFIXES name: BUGFIXES
labels: labels:
- type/bug - kind/bug
- -
name: TESTING name: TESTING
labels: labels:
- type/testing - kind/testing
-
name: TRANSLATION
labels:
- kind/translation
- -
name: BUILD name: BUILD
labels: labels:
- topic/build - kind/build
- topic/code-linting - kind/lint
- -
name: DOCS name: DOCS
labels: labels:
- type/docs - kind/docs
- -
name: MISC name: MISC
default: true default: true

View file

@ -10,8 +10,6 @@ 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
@ -24,8 +22,6 @@ 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
@ -38,7 +34,7 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: 'docker.io/node:20-bookworm' image: codeberg.org/forgejo/test_env:1.20
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
@ -46,16 +42,15 @@ jobs:
go-version: "1.20" go-version: "1.20"
- run: | - run: |
git config --add safe.directory '*' git config --add safe.directory '*'
adduser --quiet --comment forgejo --disabled-password forgejo chown -R gitea:gitea . /go
chown -R forgejo:forgejo .
- run: | - run: |
su forgejo -c 'make deps-backend' su gitea -c 'make deps-backend'
- run: | - run: |
su forgejo -c 'make backend' su gitea -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su forgejo -c 'make unit-test-coverage test-check' su gitea -c 'make unit-test-coverage test-check'
timeout-minutes: 50 timeout-minutes: 50
env: env:
RACE_ENABLED: 'true' RACE_ENABLED: 'true'
@ -64,7 +59,7 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: 'docker.io/node:20-bookworm' image: codeberg.org/forgejo/test_env:1.20
services: services:
mysql8: mysql8:
image: mysql:8-debian image: mysql:8-debian
@ -82,24 +77,17 @@ 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"
- name: install dependencies - run: |
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 '*'
adduser --quiet --comment forgejo --disabled-password forgejo chown -R gitea:gitea . /go
chown -R forgejo:forgejo .
- run: | - run: |
su forgejo -c 'make deps-backend' su gitea -c 'make deps-backend'
- run: | - run: |
su forgejo -c 'make backend' su gitea -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su forgejo -c 'make test-mysql8-migration test-mysql8' su gitea -c 'make test-mysql8-migration test-mysql8'
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata TAGS: bindata
@ -108,10 +96,10 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: 'docker.io/node:20-bookworm' image: codeberg.org/forgejo/test_env:1.20
services: services:
pgsql: pgsql:
image: 'docker.io/postgres:15' image: postgres:15
env: env:
POSTGRES_DB: test POSTGRES_DB: test
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@ -122,24 +110,17 @@ 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"
- name: install dependencies - run: |
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 '*'
adduser --quiet --comment forgejo --disabled-password forgejo chown -R gitea:gitea . /go
chown -R forgejo:forgejo .
- run: | - run: |
su forgejo -c 'make deps-backend' su gitea -c 'make deps-backend'
- run: | - run: |
su forgejo -c 'make backend' su gitea -c 'make backend'
env: env:
TAGS: bindata TAGS: bindata
- run: | - run: |
su forgejo -c 'make test-pgsql-migration test-pgsql' su gitea -c 'make test-pgsql-migration test-pgsql'
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata gogit TAGS: bindata gogit
@ -150,30 +131,23 @@ jobs:
runs-on: docker runs-on: docker
needs: [lint-backend, checks-backend] needs: [lint-backend, checks-backend]
container: container:
image: 'docker.io/node:20-bookworm' image: codeberg.org/forgejo/test_env:1.20
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"
- name: install dependencies - run: |
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 '*'
adduser --quiet --comment forgejo --disabled-password forgejo chown -R gitea:gitea . /go
chown -R forgejo:forgejo .
- run: | - run: |
su forgejo -c 'make deps-backend' su gitea -c 'make deps-backend'
- run: | - run: |
su forgejo -c 'make backend' su gitea -c 'make backend'
env: env:
TAGS: bindata gogit sqlite sqlite_unlock_notify TAGS: bindata gogit sqlite sqlite_unlock_notify
- run: | - run: |
su forgejo -c 'make test-sqlite-migration test-sqlite' su gitea -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

View file

@ -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-1 tag: v1.20.5-0
registry: https://gitea.nulo.in registry: https://gitea.nulo.in
username: Nulo username: Nulo
password: password:

View file

@ -4,33 +4,6 @@ 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

View file

@ -89,7 +89,7 @@ endif
VERSION = ${GITEA_VERSION} VERSION = ${GITEA_VERSION}
# SemVer # SemVer
FORGEJO_VERSION := 5.0.6+0-gitea-1.20.5 FORGEJO_VERSION := 5.0.5+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)"

View file

@ -20,10 +20,6 @@ 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

View file

@ -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).ID(source.ID).Delete(new(Source)); err != nil { if _, err := db.GetEngine(db.DefaultContext).Delete(source); err != nil {
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err) log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
} }
} }

View file

@ -1014,7 +1014,6 @@ 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
@ -1049,9 +1048,6 @@ 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
} }
@ -1059,7 +1055,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 || opts.IsPull != util.OptionalBoolNone { if opts.RepoID > 0 {
sess.Join("INNER", "issue", "issue.id = comment.issue_id") sess.Join("INNER", "issue", "issue.id = comment.issue_id")
} }

View file

@ -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
cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID}) 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
cond = cond.Or(userOrgPublicRepoCond(user.ID)) userOrgPublicRepoCond(user.ID),
} )
} }
return cond return cond

View file

@ -7,7 +7,6 @@ package unittest
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -29,16 +28,6 @@ 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...)
@ -48,12 +37,6 @@ 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:
@ -74,7 +57,6 @@ 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())

View file

@ -198,8 +198,6 @@ 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

View file

@ -11,6 +11,7 @@ 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"
@ -196,6 +197,39 @@ 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 {

View file

@ -0,0 +1,23 @@
// 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))
}

View file

@ -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 deleted branches without existing repository // find branches without existing repository
genericOrphanCheck("Deleted Branches without existing repository", genericOrphanCheck("Branches without existing repository",
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"), "branch", "repository", "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"),

View file

@ -7,17 +7,12 @@ 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
@ -31,18 +26,11 @@ func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList,
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
Control: func(network, ipAddr string, c syscall.RawConn) error { Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
host, port, err := net.SplitHostPort(addrOrHost) var host string
if err != nil { if host, _, err = net.SplitHostPort(addrOrHost); 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 {

View file

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

View file

@ -264,18 +264,6 @@ 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\"",

View file

@ -16,7 +16,6 @@ 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"`
} }

View file

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

View file

@ -315,6 +315,10 @@ func reqToken() func(ctx *context.APIContext) {
return return
} }
if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
}
if ctx.IsSigned { if ctx.IsSigned {
return return
} }
@ -336,6 +340,7 @@ func reqBasicAuth() func(ctx *context.APIContext) {
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return return
} }
ctx.CheckForOTP()
} }
} }
@ -682,6 +687,12 @@ 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{},
@ -1154,8 +1165,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{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue) Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues) m.Get("/pinned", 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() {
@ -1297,10 +1308,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("", org.ListTeams) m.Get("", reqToken(), org.ListTeams)
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", org.SearchTeam) m.Get("/search", reqToken(), org.SearchTeam)
}, reqToken(), reqOrgMembership()) }, 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)

View file

@ -452,24 +452,6 @@ 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() {
@ -580,10 +562,6 @@ 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))
} }

View file

@ -12,11 +12,9 @@ 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"
@ -71,11 +69,6 @@ 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{
@ -272,27 +265,12 @@ 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)
@ -379,11 +357,6 @@ 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
@ -457,11 +430,6 @@ 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
@ -580,17 +548,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
if err := comment.LoadIssue(ctx); err != nil { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
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
} }
@ -693,17 +651,7 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if err := comment.LoadIssue(ctx); err != nil { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
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 {

View file

@ -61,12 +61,6 @@ 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) {
@ -192,19 +186,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return return
} }
if err = comment.LoadIssue(ctx); err != nil { err = comment.LoadIssue(ctx)
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) {

View file

@ -155,11 +155,6 @@ 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

View file

@ -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) http.HandlerFunc { func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
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" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) next.ServeHTTP(w, req)
return return
} }
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") { if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) next.ServeHTTP(w, req)
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, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, "file not found", 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" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) next.ServeHTTP(w, req)
return return
} }
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") { if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) next.ServeHTTP(w, req)
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, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, "file not found", 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, http.StatusText(http.StatusNotFound), http.StatusNotFound) http.Error(w, "file not found", 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,4 +95,5 @@ 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)
}) })
}
} }

View file

@ -1,43 +0,0 @@
// 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())
}

View file

@ -251,6 +251,7 @@ 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 {
@ -405,9 +406,6 @@ 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

View file

@ -2971,11 +2971,6 @@ 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
@ -3042,11 +3037,6 @@ 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
@ -3173,11 +3163,6 @@ 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 {
@ -3321,16 +3306,6 @@ 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

View file

@ -125,10 +125,6 @@ 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
@ -198,19 +194,11 @@ 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 {

View file

@ -89,10 +89,6 @@ 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 {

View file

@ -592,17 +592,7 @@ func DeleteTag(ctx *context.Context) {
} }
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) { func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
id := ctx.FormInt64("id") if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil {
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 {

View file

@ -821,11 +821,6 @@ 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 {

View file

@ -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, IsInternal: util.OptionalBoolFalse}); has { if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID}); has {
redirectURL = ctx.Package.Descriptor.PackageWebLink() redirectURL = ctx.Package.Descriptor.PackageWebLink()
} }

View file

@ -175,8 +175,6 @@ 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})
@ -184,6 +182,7 @@ 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) {
@ -1392,7 +1391,19 @@ func registerRoutes(m *web.Route) {
}) })
}, ignSignInAndCsrf, lfsServerEnabled) }, ignSignInAndCsrf, lfsServerEnabled)
gitHTTPRouters(m) 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())
}) })
}) })
// ***** END: Repository ***** // ***** END: Repository *****

View file

@ -37,16 +37,12 @@ func isContainerPath(req *http.Request) bool {
} }
var ( var (
gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`) gitRawReleasePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/))`)
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 isGitRawOrAttachPath(req *http.Request) bool { func isGitRawReleaseOrLFSPath(req *http.Request) bool {
return gitRawOrAttachPathRe.MatchString(req.URL.Path) if gitRawReleasePathRe.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 {

View file

@ -85,10 +85,6 @@ 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/",
@ -108,11 +104,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 := isGitRawOrAttachOrLFSPath(req); got != tt.want { if got := isGitRawReleaseOrLFSPath(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 := isGitRawOrAttachOrLFSPath(req); got != tt.want { if got := isGitRawReleaseOrLFSPath(req); got != tt.want {
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
} }
}) })
@ -121,11 +117,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 := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer { if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt)) t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawReleasePathRe.MatchString(tt))
} }
setting.LFS.StartServer = true setting.LFS.StartServer = true
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer { if got := isGitRawReleaseOrLFSPath(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)
} }
}) })

View file

@ -15,7 +15,6 @@ 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"
) )
@ -44,7 +43,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) && !isGitRawOrAttachOrLFSPath(req) { if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
return nil, nil return nil, nil
} }
@ -133,38 +132,11 @@ 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() {
if err := validateTOTP(req, u); err != nil { store.GetData()["SkipLocalTwoFA"] = true
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
}

View file

@ -7,6 +7,7 @@ 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"
@ -215,6 +216,31 @@ 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 {

View file

@ -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) &&
!isGitRawOrAttachPath(req) { !gitRawReleasePathRe.MatchString(req.URL.Path) {
return nil, nil return nil, nil
} }

View file

@ -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) && !isGitRawOrAttachOrLFSPath(req) { if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(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)
} }

View file

@ -4,7 +4,10 @@
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"
) )
@ -13,7 +16,12 @@ 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 {
return attach.DownloadURL() 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)
} }
// ToAttachment converts models.Attachment to api.Attachment for API usage // ToAttachment converts models.Attachment to api.Attachment for API usage

View file

@ -35,7 +35,6 @@ 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
} }

View file

@ -58,10 +58,6 @@ 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
} }

View file

@ -282,8 +282,6 @@ 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{
@ -294,18 +292,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, assetID) asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, asset.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !hasBaseURL(assetDownloadURL, g.baseURL) { if !hasBaseURL(asset.DownloadURL, g.baseURL) {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL) WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
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", assetDownloadURL, nil) req, err := http.NewRequest("GET", asset.DownloadURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -309,7 +309,6 @@ 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,
@ -317,13 +316,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, assetID, gitlab.WithContext(g.ctx)) link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, asset.ID, 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", assetID, g, link.URL) WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL)
return io.NopCloser(strings.NewReader(link.URL)), nil return io.NopCloser(strings.NewReader(link.URL)), nil
} }

View file

@ -45,9 +45,6 @@ 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 := "#"

View file

@ -243,7 +243,7 @@ var (
hostMatchers []glob.Glob hostMatchers []glob.Glob
) )
func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) { func webhookProxy() func(req *http.Request) (*url.URL, error) {
if setting.Webhook.ProxyURL == "" { if setting.Webhook.ProxyURL == "" {
return proxy.Proxy() return proxy.Proxy()
} }
@ -261,9 +261,6 @@ func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request)
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)
} }
} }
@ -285,8 +282,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(allowedHostMatcher), Proxy: webhookProxy(),
DialContext: hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed), DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
}, },
} }

View file

@ -14,72 +14,35 @@ 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"}
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com") kases := map[string]string{
"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
tests := []struct { "http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080",
req string "http://github.com/a/b": "",
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)
got := "" u, err := webhookProxy()(req)
if u != nil { assert.NoError(t, err)
got = u.String() if proxyURL == "" {
assert.Nil(t, u)
} else {
assert.EqualValues(t, proxyURL, u.String())
} }
assert.Equal(t, tt.want, got)
})
} }
} }

View file

@ -173,12 +173,6 @@ 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",

View file

@ -256,12 +256,6 @@ 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)

View file

@ -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 / file /audio / media MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive
Content struct { Content struct {
Text string `json:"text"` Text string `json:"text"`
} `json:"content"` } `json:"content"`
@ -158,12 +158,6 @@ 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)

View file

@ -230,24 +230,6 @@ 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) {

View file

@ -210,21 +210,6 @@ 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)

View file

@ -296,20 +296,6 @@ 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)

View file

@ -104,10 +104,6 @@ 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)

View file

@ -22,7 +22,6 @@ 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) {
@ -54,8 +53,6 @@ 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
} }

View file

@ -171,12 +171,6 @@ 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

View file

@ -186,12 +186,6 @@ 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)

View file

@ -179,12 +179,6 @@ 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)

View file

@ -325,6 +325,8 @@
<!-- 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>

View file

@ -20249,10 +20249,6 @@
"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",

View file

@ -35,14 +35,6 @@ 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)

View file

@ -174,29 +174,15 @@ 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{ID: 1008}, comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
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)
@ -213,25 +199,14 @@ 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{ID: 1008}, comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
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)

View file

@ -12,7 +12,6 @@ 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"
@ -108,27 +107,6 @@ 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",

View file

@ -72,17 +72,6 @@ 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) {

View file

@ -1,59 +0,0 @@
// 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)
}

View file

@ -1,9 +0,0 @@
-
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

View file

@ -205,111 +205,6 @@ 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")
@ -661,45 +556,3 @@ 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)
}

View file

@ -89,44 +89,6 @@ 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)()

View file

@ -264,13 +264,3 @@ 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,
},
)
}

View file

@ -4,11 +4,9 @@ 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');