mirror of
https://github.com/go-gitea/gitea
synced 2026-02-05 07:11:51 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f831540ebd | ||
|
|
1e9a7656e0 | ||
|
|
088d746636 | ||
|
|
c208b1180e | ||
|
|
dfd511faf3 | ||
|
|
41b2d0be93 | ||
|
|
4d5854b216 | ||
|
|
439e071acf | ||
|
|
69ea554e23 | ||
|
|
c077a084d7 | ||
|
|
dfd960f22a | ||
|
|
2978b435bb | ||
|
|
00705da102 | ||
|
|
4a48370d91 | ||
|
|
1dedf9bba0 | ||
|
|
2147bfde05 | ||
|
|
a1c232cae3 | ||
|
|
63512cd15d | ||
|
|
71803d33e3 | ||
|
|
a954cc3fb9 | ||
|
|
ca4418eff1 | ||
|
|
80c0c88152 | ||
|
|
171950a0d4 | ||
|
|
7b96f71bc7 | ||
|
|
e6d1afaee3 | ||
|
|
1d4c193df5 | ||
|
|
4ffa683820 | ||
|
|
973b7f6298 |
+12
-16
@@ -13,46 +13,42 @@ groups:
|
||||
-
|
||||
name: BREAKING
|
||||
labels:
|
||||
- kind/breaking
|
||||
- pr/breaking
|
||||
-
|
||||
name: SECURITY
|
||||
labels:
|
||||
- kind/security
|
||||
- topic/security
|
||||
-
|
||||
name: FEATURES
|
||||
labels:
|
||||
- kind/feature
|
||||
- type/feature
|
||||
-
|
||||
name: API
|
||||
labels:
|
||||
- kind/api
|
||||
- modifies/api
|
||||
-
|
||||
name: ENHANCEMENTS
|
||||
labels:
|
||||
- kind/enhancement
|
||||
- kind/refactor
|
||||
- kind/ui
|
||||
- type/enhancement
|
||||
- type/refactoring
|
||||
- topic/ui
|
||||
-
|
||||
name: BUGFIXES
|
||||
labels:
|
||||
- kind/bug
|
||||
- type/bug
|
||||
-
|
||||
name: TESTING
|
||||
labels:
|
||||
- kind/testing
|
||||
-
|
||||
name: TRANSLATION
|
||||
labels:
|
||||
- kind/translation
|
||||
- type/testing
|
||||
-
|
||||
name: BUILD
|
||||
labels:
|
||||
- kind/build
|
||||
- kind/lint
|
||||
- topic/build
|
||||
- topic/code-linting
|
||||
-
|
||||
name: DOCS
|
||||
labels:
|
||||
- kind/docs
|
||||
- type/docs
|
||||
-
|
||||
name: MISC
|
||||
default: true
|
||||
|
||||
-426
@@ -1,426 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: release-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
workspace:
|
||||
base: /source
|
||||
path: /
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
|
||||
volumes:
|
||||
- name: deps
|
||||
temp: {}
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
pull: always
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: deps-frontend
|
||||
image: node:20
|
||||
pull: always
|
||||
commands:
|
||||
- make deps-frontend
|
||||
|
||||
- name: deps-backend
|
||||
image: gitea/test_env:linux-1.20-amd64
|
||||
pull: always
|
||||
commands:
|
||||
- make deps-backend
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
|
||||
- name: static
|
||||
image: techknowlogick/xgo:go-1.20.x
|
||||
pull: always
|
||||
commands:
|
||||
# Upgrade to node 20 once https://github.com/techknowlogick/xgo/issues/163 is resolved
|
||||
- curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs
|
||||
- export PATH=$PATH:$GOPATH/bin
|
||||
- make release
|
||||
environment:
|
||||
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
depends_on: [fetch-tags]
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
|
||||
- name: gpg-sign
|
||||
image: plugins/gpgsign:1
|
||||
pull: always
|
||||
settings:
|
||||
detach_sign: true
|
||||
excludes:
|
||||
- "dist/release/*.sha256"
|
||||
files:
|
||||
- "dist/release/*"
|
||||
environment:
|
||||
GPGSIGN_KEY:
|
||||
from_secret: gpgsign_key
|
||||
GPGSIGN_PASSPHRASE:
|
||||
from_secret: gpgsign_passphrase
|
||||
depends_on: [static]
|
||||
|
||||
- name: release-tag
|
||||
image: woodpeckerci/plugin-s3:latest
|
||||
pull: always
|
||||
settings:
|
||||
acl:
|
||||
from_secret: aws_s3_acl
|
||||
region:
|
||||
from_secret: aws_s3_region
|
||||
bucket:
|
||||
from_secret: aws_s3_bucket
|
||||
endpoint:
|
||||
from_secret: aws_s3_endpoint
|
||||
path_style:
|
||||
from_secret: aws_s3_path_style
|
||||
source: "dist/release/*"
|
||||
strip_prefix: dist/release/
|
||||
target: "/gitea/${DRONE_TAG##v}"
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: aws_secret_access_key
|
||||
depends_on: [gpg-sign]
|
||||
|
||||
- name: github
|
||||
image: plugins/github-release:latest
|
||||
pull: always
|
||||
settings:
|
||||
files:
|
||||
- "dist/release/*"
|
||||
file_exists: overwrite
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
depends_on: [gpg-sign]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-linux-amd64-release-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
include:
|
||||
- "refs/tags/**"
|
||||
exclude:
|
||||
- "refs/tags/**-rc*"
|
||||
paths:
|
||||
exclude:
|
||||
- "docs/**"
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
pull: always
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: publish
|
||||
image: plugins/docker:latest
|
||||
pull: always
|
||||
settings:
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: publish-rootless
|
||||
image: plugins/docker:latest
|
||||
settings:
|
||||
dockerfile: Dockerfile.rootless
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64-rootless
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
---
|
||||
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-linux-amd64-release-candidate-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- "refs/tags/**-rc*"
|
||||
paths:
|
||||
exclude:
|
||||
- "docs/**"
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
pull: always
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: publish
|
||||
image: plugins/docker:latest
|
||||
pull: always
|
||||
settings:
|
||||
tags: ${DRONE_TAG##v}-linux-amd64
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: publish-rootless
|
||||
image: plugins/docker:latest
|
||||
settings:
|
||||
dockerfile: Dockerfile.rootless
|
||||
tags: ${DRONE_TAG##v}-linux-amd64-rootless
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-linux-arm64-release-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
include:
|
||||
- "refs/tags/**"
|
||||
exclude:
|
||||
- "refs/tags/**-rc*"
|
||||
paths:
|
||||
exclude:
|
||||
- "docs/**"
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
pull: always
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: publish
|
||||
image: plugins/docker:latest
|
||||
pull: always
|
||||
settings:
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm64
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: publish-rootless
|
||||
image: plugins/docker:latest
|
||||
settings:
|
||||
dockerfile: Dockerfile.rootless
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm64-rootless
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-linux-arm64-release-candidate-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- "refs/tags/**-rc*"
|
||||
paths:
|
||||
exclude:
|
||||
- "docs/**"
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
pull: always
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
|
||||
- name: publish
|
||||
image: plugins/docker:latest
|
||||
pull: always
|
||||
settings:
|
||||
tags: ${DRONE_TAG##v}-linux-arm64
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
- name: publish-rootless
|
||||
image: plugins/docker:latest
|
||||
settings:
|
||||
dockerfile: Dockerfile.rootless
|
||||
tags: ${DRONE_TAG##v}-linux-arm64-rootless
|
||||
repo: gitea/gitea
|
||||
build_args:
|
||||
- GOPROXY=https://goproxy.io
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
environment:
|
||||
PLUGIN_MIRROR:
|
||||
from_secret: plugin_mirror
|
||||
DOCKER_BUILDKIT: 1
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-manifest-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: manifest-rootless
|
||||
image: plugins/manifest
|
||||
pull: always
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: docker/manifest.rootless.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
|
||||
- name: manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: docker/manifest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
paths:
|
||||
exclude:
|
||||
- "docs/**"
|
||||
|
||||
depends_on:
|
||||
- docker-linux-amd64-release-version
|
||||
- docker-linux-amd64-release-candidate-version
|
||||
- docker-linux-arm64-release-version
|
||||
- docker-linux-arm64-release-candidate-version
|
||||
@@ -0,0 +1,5 @@
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- actuated-4cpu-8gb
|
||||
- actuated-4cpu-16gb
|
||||
- nscloud
|
||||
@@ -0,0 +1,146 @@
|
||||
name: release-tag-version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v1.*"
|
||||
- "!v1*-rc*"
|
||||
- "!v1*-dev"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: nscloud
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: make deps-frontend deps-backend
|
||||
# xgo build
|
||||
- run: make release
|
||||
env:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
- name: sign binaries
|
||||
run: |
|
||||
for f in dist/release/*; do
|
||||
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
|
||||
done
|
||||
# clean branch name to get the folder name in S3
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# this will generate tags in the following format:
|
||||
# since it's not a main stable version, just generation 1.x and 1.x.x
|
||||
# latest
|
||||
# 1
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: build rootful docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
docker-rootless:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# this will generate tags in the following format (with -rootless suffix added):
|
||||
# since it's not a main stable version, just generation 1.x and 1.x.x
|
||||
# latest
|
||||
# 1
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: Dockerfile.rootless
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -4,6 +4,38 @@ This changelog goes through all the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.20.6](https://github.com/go-gitea/gitea/releases/tag/1.20.6) - 2023-11-26
|
||||
|
||||
* SECURITY
|
||||
* Fix comment permissions (#28213) (#28217)
|
||||
* Dont leak private users via extensions (#28023) (#28028)
|
||||
* Unify two factor check (#27915) (#27939)
|
||||
* Support allowed hosts for webhook to work with proxy (#27655) (#27674)
|
||||
* BUGFIXES
|
||||
* Fix no ActionTaskOutput table waring (#28149) (#28151)
|
||||
* Restricted users only see repos in orgs which their team was assigned to (#28025) (#28050)
|
||||
* Fix DownloadFunc when migrating releases (#27887) (#27889)
|
||||
* Fix http protocol auth (#27875) (#27878)
|
||||
* Revert "fix orphan check for deleted branch (#27310) (#27320)" (#27763)
|
||||
* Fix label render containing invalid HTML (#27752) (#27761)
|
||||
* Fix poster is not loaded in get default merge message (#27657) (#27665)
|
||||
* Fix 404 when deleting Docker package with an internal version (#27615) (#27629)
|
||||
* Fix attachment download bug (#27486) (#27570)
|
||||
* When comparing with an non-exist repository, return 404 but 500 (#27437) (#27441)
|
||||
* API
|
||||
* Fix package webhook (#27839) (#27854)
|
||||
* Fix org team endpoint (#27721) (#27729)
|
||||
* ENHANCEMENTS
|
||||
* Render email addresses as such if followed by punctuation (#27987) (#27991)
|
||||
* Fix mermaid flowchart margin issue (#27503) (#27517)
|
||||
* Fix panic in storageHandler (#27446) (#27478)
|
||||
* DOCS
|
||||
* Update agit-support.en-us.md (#27652)
|
||||
* MISC
|
||||
* Fix wrong xorm Delete usage(backport for 1.20) (#28003)
|
||||
* Remove duplicated button in Install web page (#27941)
|
||||
* Avoid run change title process when the title is same (#27467) (#27557)
|
||||
|
||||
## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/1.20.5) - 2023-10-03
|
||||
|
||||
* ENHANCEMENTS
|
||||
|
||||
@@ -337,7 +337,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
||||
- `SSH_AUTHORIZED_PRINCIPALS_ALLOW`: **off** or **username, email**: \[off, username, email, anything\]: Specify the principals values that users are allowed to use as principal. When set to `anything` no checks are done on the principal string. When set to `off` authorized principal are not allowed to be set.
|
||||
- `SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE`: **false/true**: Gitea will create a authorized_principals file by default when it is not using the internal ssh server and `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
|
||||
- `SSH_AUTHORIZED_PRINCIPALS_BACKUP`: **false/true**: Enable SSH Authorized Principals Backup when rewriting all keys, default is true if `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
|
||||
- `SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE`: **{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}**: Set the template for the command to passed on authorized keys. Possible keys are: AppPath, AppWorkPath, CustomConf, CustomPath, Key - where Key is a `models/asymkey.PublicKey` and the others are strings which are shellquoted.
|
||||
- `SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE`: **`{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}`**: Set the template for the command to passed on authorized keys. Possible keys are: AppPath, AppWorkPath, CustomConf, CustomPath, Key - where Key is a `models/asymkey.PublicKey` and the others are strings which are shellquoted.
|
||||
- `SSH_SERVER_CIPHERS`: **chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com**: For the built-in SSH server, choose the ciphers to support for SSH connections, for system SSH this setting has no effect.
|
||||
- `SSH_SERVER_KEY_EXCHANGES`: **curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1**: For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, for system SSH this setting has no effect.
|
||||
- `SSH_SERVER_MACS`: **hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1**: For the built-in SSH server, choose the MACs to support for SSH connections, for system SSH this setting has no effect
|
||||
@@ -1384,7 +1384,7 @@ Please note that using `self` is not recommended for most cases, as it could mak
|
||||
Additionally, it requires you to mirror all the actions you need to your Gitea instance, which may not be worth it.
|
||||
Therefore, please use `self` only if you understand what you are doing.
|
||||
|
||||
In earlier versions (<= 1.19), `DEFAULT_ACTIONS_URL` cound be set to any custom URLs like `https://gitea.com` or `http://your-git-server,https://gitea.com`, and the default value was `https://gitea.com`.
|
||||
In earlier versions (`<= 1.19`), `DEFAULT_ACTIONS_URL` cound be set to any custom URLs like `https://gitea.com` or `http://your-git-server,https://gitea.com`, and the default value was `https://gitea.com`.
|
||||
However, later updates removed those options, and now the only options are `github` and `self`, with the default value being `github`.
|
||||
However, if you want to use actions from other git server, you can use a complete URL in `uses` field, it's supported by Gitea (but not GitHub).
|
||||
Like `uses: https://gitea.com/actions/checkout@v3` or `uses: http://your-git-server/actions/checkout@v3`.
|
||||
|
||||
@@ -55,7 +55,7 @@ PASSWD = `password`
|
||||
|
||||
要发送测试邮件以验证设置,请转到 Gitea > 站点管理 > 配置 > SMTP 邮件配置。
|
||||
|
||||
有关所有选项的完整列表,请查看[配置速查表](doc/administration/config-cheat-sheet.zh-cn.md)。
|
||||
有关所有选项的完整列表,请查看[配置速查表](administration/config-cheat-sheet.md)。
|
||||
|
||||
请注意:只有在使用 TLS 或 `HOST=localhost` 加密 SMTP 服务器通信时才支持身份验证。TLS 加密可以通过以下方式进行:
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ All Gitea instances have the built-in API and there is no way to disable it comp
|
||||
You can, however, disable showing its documentation by setting `ENABLE_SWAGGER` to `false` in the `api` section of your `app.ini`.
|
||||
For more information, refer to Gitea's [API docs](development/api-usage.md).
|
||||
|
||||
You can see the latest API (for example) on <https://try.gitea.io/api/swagger>.
|
||||
You can see the latest API (for example) on https://try.gitea.io/api/swagger
|
||||
|
||||
You can also see an example of the `swagger.json` file at <https://try.gitea.io/swagger.v1.json>.
|
||||
You can also see an example of the `swagger.json` file at https://try.gitea.io/swagger.v1.json
|
||||
|
||||
## Adjusting your server for public/private use
|
||||
|
||||
|
||||
@@ -142,9 +142,9 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供
|
||||
但是,您可以在app.ini的api部分将ENABLE_SWAGGER设置为false,以禁用其文档显示。
|
||||
有关更多信息,请参阅Gitea的[API文档](development/api-usage.md)。
|
||||
|
||||
您可以在上查看最新的API(例如)<https://try.gitea.io/api/swagger>。
|
||||
您可以在上查看最新的API(例如)https://try.gitea.io/api/swagger
|
||||
|
||||
您还可以在上查看`swagger.json`文件的示例 <https://try.gitea.io/swagger.v1.json>。
|
||||
您还可以在上查看`swagger.json`文件的示例 https://try.gitea.io/swagger.v1.json
|
||||
|
||||
## 调整服务器用于公共/私有使用
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ administrative user.
|
||||
field is set to `mail.com`, then Gitea will expect the `user email` field
|
||||
for an authenticated GIT instance to be `gituser@mail.com`.[^2]
|
||||
|
||||
**Note**: PAM support is added via [build-time flags](installation/install-from-source.md#build),
|
||||
**Note**: PAM support is added via [build-time flags](installation/from-source.md#build),
|
||||
and the official binaries provided do not have this enabled. PAM requires that
|
||||
the necessary libpam dynamic library be available and the necessary PAM
|
||||
development headers be accessible to the compiler.
|
||||
|
||||
@@ -20,6 +20,10 @@ type ActionTaskOutput struct {
|
||||
OutputValue string `xorm:"MEDIUMTEXT"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionTaskOutput))
|
||||
}
|
||||
|
||||
// FindTaskOutputByTaskID returns the outputs of the task.
|
||||
func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) {
|
||||
var outputs []*ActionTaskOutput
|
||||
|
||||
@@ -93,9 +93,9 @@ func CountUserGPGKeys(userID int64) (int64, error) {
|
||||
}
|
||||
|
||||
// GetGPGKeyByID returns public key by given ID.
|
||||
func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
|
||||
func GetGPGKeyForUserByID(ownerID, keyID int64) (*GPGKey, error) {
|
||||
key := new(GPGKey)
|
||||
has, err := db.GetEngine(db.DefaultContext).ID(keyID).Get(key)
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("id=? AND owner_id=?", keyID, ownerID).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -225,7 +225,7 @@ func deleteGPGKey(ctx context.Context, keyID string) (int64, error) {
|
||||
|
||||
// DeleteGPGKey deletes GPG key information in database.
|
||||
func DeleteGPGKey(doer *user_model.User, id int64) (err error) {
|
||||
key, err := GetGPGKeyByID(id)
|
||||
key, err := GetGPGKeyForUserByID(doer.ID, id)
|
||||
if err != nil {
|
||||
if IsErrGPGKeyNotExist(err) {
|
||||
return nil
|
||||
@@ -233,11 +233,6 @@ func DeleteGPGKey(doer *user_model.User, id int64) (err error) {
|
||||
return fmt.Errorf("GetPublicKeyByID: %w", err)
|
||||
}
|
||||
|
||||
// Check if user has access to delete this key.
|
||||
if !doer.IsAdmin && doer.ID != key.OwnerID {
|
||||
return ErrGPGKeyAccessDenied{doer.ID, key.ID}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -232,7 +232,7 @@ func CreateSource(source *Source) error {
|
||||
err = registerableSource.RegisterSource()
|
||||
if err != nil {
|
||||
// remove the AuthSource in case of errors while registering configuration
|
||||
if _, err := db.GetEngine(db.DefaultContext).Delete(source); err != nil {
|
||||
if _, err := db.GetEngine(db.DefaultContext).ID(source.ID).Delete(new(Source)); err != nil {
|
||||
log.Error("CreateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,3 +66,12 @@
|
||||
tree_path: "README.md"
|
||||
created_unix: 946684812
|
||||
invalidated: true
|
||||
|
||||
-
|
||||
id: 8
|
||||
type: 0 # comment
|
||||
poster_id: 2
|
||||
issue_id: 4 # in repo_id 2
|
||||
content: "comment in private pository"
|
||||
created_unix: 946684811
|
||||
updated_unix: 946684811
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
priority: 0
|
||||
is_closed: true
|
||||
is_pull: false
|
||||
num_comments: 0
|
||||
num_comments: 1
|
||||
created_unix: 946684830
|
||||
updated_unix: 978307200
|
||||
is_locked: false
|
||||
|
||||
@@ -1014,6 +1014,7 @@ type FindCommentsOptions struct {
|
||||
Type CommentType
|
||||
IssueIDs []int64
|
||||
Invalidated util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
}
|
||||
|
||||
// ToConds implements FindOptions interface
|
||||
@@ -1048,6 +1049,9 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
||||
if !opts.Invalidated.IsNone() {
|
||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
||||
}
|
||||
if opts.IsPull != util.OptionalBoolNone {
|
||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
@@ -1055,7 +1059,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
if opts.RepoID > 0 {
|
||||
if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
|
||||
|
||||
@@ -218,9 +218,9 @@ func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistor
|
||||
}
|
||||
|
||||
// GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
|
||||
func GetIssueContentHistoryAndPrev(dbCtx context.Context, id int64) (history, prevHistory *ContentHistory, err error) {
|
||||
func GetIssueContentHistoryAndPrev(dbCtx context.Context, issueID, id int64) (history, prevHistory *ContentHistory, err error) {
|
||||
history = &ContentHistory{}
|
||||
has, err := db.GetEngine(dbCtx).ID(id).Get(history)
|
||||
has, err := db.GetEngine(dbCtx).Where("id=? AND issue_id=?", id, issueID).Get(history)
|
||||
if err != nil {
|
||||
log.Error("failed to get issue content history %v. err=%v", id, err)
|
||||
return nil, nil, err
|
||||
|
||||
@@ -58,13 +58,13 @@ func TestContentHistory(t *testing.T) {
|
||||
hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
|
||||
assert.False(t, hasHistory2)
|
||||
|
||||
h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 5, h6Prev.ID)
|
||||
|
||||
// soft-delete
|
||||
_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5)
|
||||
h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 4, h6Prev.ID)
|
||||
|
||||
|
||||
@@ -306,6 +306,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetProjectForRepoByID returns the projects in a repository
|
||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectNotExist{ID: id}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// UpdateProject updates project properties
|
||||
func UpdateProject(ctx context.Context, p *Project) error {
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
|
||||
@@ -201,6 +201,21 @@ func GetReleaseByID(ctx context.Context, id int64) (*Release, error) {
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetReleaseForRepoByID returns release with given ID.
|
||||
func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, error) {
|
||||
rel := new(Release)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("id=? AND repo_id=?", id, repoID).
|
||||
Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrReleaseNotExist{id, ""}
|
||||
}
|
||||
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// FindReleasesOptions describes the conditions to Find releases
|
||||
type FindReleasesOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
@@ -637,12 +637,12 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||
userOrgTeamUnitRepoCond("`repository`.id", user.ID, unitType),
|
||||
)
|
||||
}
|
||||
cond = cond.Or(
|
||||
// 4. Repositories that we directly own
|
||||
builder.Eq{"`repository`.owner_id": user.ID},
|
||||
// 4. Repositories that we directly own
|
||||
cond = cond.Or(builder.Eq{"`repository`.owner_id": user.ID})
|
||||
if !user.IsRestricted {
|
||||
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
||||
userOrgPublicRepoCond(user.ID),
|
||||
)
|
||||
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return cond
|
||||
|
||||
+34
-33
@@ -392,39 +392,40 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
|
||||
return db.Insert(ctx, ws)
|
||||
}
|
||||
|
||||
// getWebhook uses argument bean as query condition,
|
||||
// ID must be specified and do not assign unnecessary fields.
|
||||
func getWebhook(bean *Webhook) (*Webhook, error) {
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(bean)
|
||||
// GetWebhookByID returns webhook of repository by given ID.
|
||||
func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
|
||||
bean := new(Webhook)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(bean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: bean.ID}
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return bean, nil
|
||||
}
|
||||
|
||||
// GetWebhookByID returns webhook of repository by given ID.
|
||||
func GetWebhookByID(id int64) (*Webhook, error) {
|
||||
return getWebhook(&Webhook{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWebhookByRepoID returns webhook of repository by given ID.
|
||||
func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
|
||||
return getWebhook(&Webhook{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
})
|
||||
webhook := new(Webhook)
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("id=? AND repo_id=?", id, repoID).Get(webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
|
||||
func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) {
|
||||
return getWebhook(&Webhook{
|
||||
ID: id,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
webhook := new(Webhook)
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("id=? AND owner_id=?", id, ownerID).Get(webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
|
||||
@@ -482,20 +483,20 @@ func UpdateWebhookLastStatus(w *Webhook) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteWebhook uses argument bean as query condition,
|
||||
// DeleteWebhookByID uses argument bean as query condition,
|
||||
// ID must be specified and do not assign unnecessary fields.
|
||||
func deleteWebhook(bean *Webhook) (err error) {
|
||||
func DeleteWebhookByID(id int64) (err error) {
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if count, err := db.DeleteByBean(ctx, bean); err != nil {
|
||||
if count, err := db.DeleteByID(ctx, id, new(Webhook)); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
return ErrWebhookNotExist{ID: bean.ID}
|
||||
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: bean.ID}); err != nil {
|
||||
return ErrWebhookNotExist{ID: id}
|
||||
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -504,16 +505,16 @@ func deleteWebhook(bean *Webhook) (err error) {
|
||||
|
||||
// DeleteWebhookByRepoID deletes webhook of repository by given ID.
|
||||
func DeleteWebhookByRepoID(repoID, id int64) error {
|
||||
return deleteWebhook(&Webhook{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
})
|
||||
if _, err := GetWebhookByRepoID(repoID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return DeleteWebhookByID(id)
|
||||
}
|
||||
|
||||
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
|
||||
func DeleteWebhookByOwnerID(ownerID, id int64) error {
|
||||
return deleteWebhook(&Webhook{
|
||||
ID: id,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
if _, err := GetWebhookByOwnerID(ownerID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return DeleteWebhookByID(id)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -197,32 +196,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckForOTP validates OTP
|
||||
func (ctx *APIContext) CheckForOTP() {
|
||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
||||
return // Skip 2FA
|
||||
}
|
||||
|
||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
||||
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(otpHeader)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.Error(http.StatusUnauthorized, "", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// APIContexter returns apicontext as middleware
|
||||
func APIContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
||||
@@ -168,9 +168,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
||||
// find protected branches without existing repository
|
||||
genericOrphanCheck("Protected Branches without existing repository",
|
||||
"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
|
||||
// find branches without existing repository
|
||||
genericOrphanCheck("Branches without existing repository",
|
||||
"branch", "repository", "branch.repo_id=repository.id"),
|
||||
// find deleted branches without existing repository
|
||||
genericOrphanCheck("Deleted Branches without existing repository",
|
||||
"deleted_branch", "repository", "deleted_branch.repo_id=repository.id"),
|
||||
// find LFS locks without existing repository
|
||||
genericOrphanCheck("LFS locks without existing repository",
|
||||
"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
|
||||
|
||||
@@ -7,12 +7,17 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
|
||||
func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return NewDialContextWithProxy(usage, allowList, blockList, nil)
|
||||
}
|
||||
|
||||
func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// How Go HTTP Client works with redirection:
|
||||
// transport.RoundTrip URL=http://domain.com, Host=domain.com
|
||||
// transport.DialContext addrOrHost=domain.com:80
|
||||
@@ -26,11 +31,18 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
|
||||
Control: func(network, ipAddr string, c syscall.RawConn) (err error) {
|
||||
var host string
|
||||
if host, _, err = net.SplitHostPort(addrOrHost); err != nil {
|
||||
Control: func(network, ipAddr string, c syscall.RawConn) error {
|
||||
host, port, err := net.SplitHostPort(addrOrHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
// Always allow the host of the proxy, but only on the specified port.
|
||||
if host == proxy.Hostname() && port == proxy.Port() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// in Control func, the addr was already resolved to IP:PORT format, there is no cost to do ResolveTCPAddr here
|
||||
tcpAddr, err := net.ResolveTCPAddr(network, ipAddr)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ var (
|
||||
// well as the HTML5 spec:
|
||||
// http://spec.commonmark.org/0.28/#email-address
|
||||
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
|
||||
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|\\.(\\s|$))")
|
||||
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
|
||||
|
||||
// blackfriday extensions create IDs like fn:user-content-footnote
|
||||
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
|
||||
|
||||
@@ -264,6 +264,18 @@ func TestRender_email(t *testing.T) {
|
||||
"send email to info@gitea.co.uk.",
|
||||
`<p>send email to <a href="mailto:info@gitea.co.uk" rel="nofollow">info@gitea.co.uk</a>.</p>`)
|
||||
|
||||
test(
|
||||
`j.doe@example.com,
|
||||
j.doe@example.com.
|
||||
j.doe@example.com;
|
||||
j.doe@example.com?
|
||||
j.doe@example.com!`,
|
||||
`<p><a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>,<br/>
|
||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>.<br/>
|
||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>;<br/>
|
||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>?<br/>
|
||||
<a href="mailto:j.doe@example.com" rel="nofollow">j.doe@example.com</a>!</p>`)
|
||||
|
||||
// Test that should *not* be turned into email links
|
||||
test(
|
||||
"\"info@gitea.com\"",
|
||||
|
||||
@@ -16,6 +16,7 @@ type Package struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
// swagger:strfmt date-time
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
|
||||
|
||||
s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
||||
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
|
||||
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"</span>",
|
||||
description,
|
||||
textColor, scopeColor, scopeText,
|
||||
|
||||
+6
-17
@@ -315,10 +315,6 @@ func reqToken() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.IsBasicAuth {
|
||||
ctx.CheckForOTP()
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
return
|
||||
}
|
||||
@@ -340,7 +336,6 @@ func reqBasicAuth() func(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
|
||||
return
|
||||
}
|
||||
ctx.CheckForOTP()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,12 +682,6 @@ func bind[T any](_ T) any {
|
||||
}
|
||||
}
|
||||
|
||||
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
|
||||
// in the session (if there is a user id stored in session other plugins might return the user
|
||||
// object for that id).
|
||||
//
|
||||
// The Session plugin is expected to be executed second, in order to skip authentication
|
||||
// for users that have already signed in.
|
||||
func buildAuthGroup() *auth.Group {
|
||||
group := auth.NewGroup(
|
||||
&auth.OAuth2{},
|
||||
@@ -1159,8 +1148,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Group("/issues", func() {
|
||||
m.Combo("").Get(repo.ListIssues).
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
|
||||
m.Get("/pinned", repo.ListPinnedIssues)
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||
m.Group("/comments", func() {
|
||||
m.Get("", repo.ListRepoIssueComments)
|
||||
m.Group("/{id}", func() {
|
||||
@@ -1302,10 +1291,10 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
|
||||
})
|
||||
m.Group("/teams", func() {
|
||||
m.Get("", reqToken(), org.ListTeams)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", reqToken(), org.SearchTeam)
|
||||
}, reqOrgMembership())
|
||||
m.Get("", org.ListTeams)
|
||||
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", org.SearchTeam)
|
||||
}, reqToken(), reqOrgMembership())
|
||||
m.Group("/labels", func() {
|
||||
m.Get("", org.ListLabels)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
||||
|
||||
@@ -451,6 +451,24 @@ func ListIssues(ctx *context.APIContext) {
|
||||
isPull = util.OptionalBoolNone
|
||||
}
|
||||
|
||||
if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if isPull == util.OptionalBoolNone {
|
||||
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if !canReadIssues && !canReadPulls {
|
||||
ctx.NotFound()
|
||||
return
|
||||
} else if !canReadIssues {
|
||||
isPull = util.OptionalBoolTrue
|
||||
} else if !canReadPulls {
|
||||
isPull = util.OptionalBoolFalse
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we should be more efficient here
|
||||
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||
if ctx.Written() {
|
||||
@@ -561,6 +579,10 @@ func GetIssue(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@@ -69,6 +71,11 @@ func ListIssueComments(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
@@ -265,12 +272,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if canReadIssue && canReadPull {
|
||||
isPull = util.OptionalBoolNone
|
||||
} else if canReadIssue {
|
||||
isPull = util.OptionalBoolFalse
|
||||
} else if canReadPull {
|
||||
isPull = util.OptionalBoolTrue
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Since: since,
|
||||
Before: before,
|
||||
IsPull: isPull,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
@@ -357,6 +379,11 @@ func CreateIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
||||
return
|
||||
@@ -426,6 +453,11 @@ func GetIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Type != issues_model.CommentTypeComment {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
@@ -544,7 +576,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -647,7 +689,17 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if comment.Type != issues_model.CommentTypeComment {
|
||||
|
||||
@@ -325,6 +325,10 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
return nil
|
||||
}
|
||||
|
||||
comment.Issue.Repo = ctx.Repo.Repository
|
||||
|
||||
return comment
|
||||
|
||||
@@ -59,6 +59,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
@@ -184,9 +190,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||
return
|
||||
}
|
||||
|
||||
err = comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
if err = comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||
|
||||
@@ -155,6 +155,12 @@ func GetDeployKey(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// this check make it more consistent
|
||||
if key.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err = key.GetContent(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetContent", err)
|
||||
return
|
||||
|
||||
@@ -49,13 +49,12 @@ func GetRelease(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
id := ctx.ParamsInt64(":id")
|
||||
release, err := repo_model.GetReleaseByID(ctx, id)
|
||||
release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) ||
|
||||
release.IsTag || release.RepoID != ctx.Repo.Repository.ID {
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
@@ -313,13 +312,12 @@ func EditRelease(ctx *context.APIContext) {
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditReleaseOption)
|
||||
id := ctx.ParamsInt64(":id")
|
||||
rel, err := repo_model.GetReleaseByID(ctx, id)
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) ||
|
||||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
@@ -391,17 +389,16 @@ func DeleteRelease(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
id := ctx.ParamsInt64(":id")
|
||||
rel, err := repo_model.GetReleaseByID(ctx, id)
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) ||
|
||||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if err := release_service.DeleteReleaseByID(ctx, id, ctx.Doer, false); err != nil {
|
||||
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil {
|
||||
if models.IsErrProtectedTagName(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
|
||||
return
|
||||
|
||||
@@ -17,6 +17,23 @@ import (
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
|
||||
release, err := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.NotFound()
|
||||
return false
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
|
||||
return false
|
||||
}
|
||||
if release.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetReleaseAttachment gets a single attachment of the release
|
||||
func GetReleaseAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoGetReleaseAttachment
|
||||
@@ -52,6 +69,10 @@ func GetReleaseAttachment(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/Attachment"
|
||||
|
||||
releaseID := ctx.ParamsInt64(":id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.ParamsInt64(":attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
@@ -170,13 +191,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.ParamsInt64(":id")
|
||||
release, err := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,7 +212,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: release.RepoID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ReleaseID: releaseID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -256,6 +271,10 @@ func EditReleaseAttachment(ctx *context.APIContext) {
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.ParamsInt64(":id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.ParamsInt64(":attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
@@ -318,6 +337,10 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.ParamsInt64(":id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.ParamsInt64(":attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
|
||||
@@ -112,7 +112,7 @@ func DeleteReleaseByTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, release.ID, ctx.Doer, false); err != nil {
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil {
|
||||
if models.IsErrProtectedTagName(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
|
||||
return
|
||||
|
||||
@@ -264,7 +264,7 @@ func DeleteTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, tag.ID, ctx.Doer, true); err != nil {
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil {
|
||||
if models.IsErrProtectedTagName(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
|
||||
return
|
||||
|
||||
@@ -337,6 +337,10 @@ func GetOauth2Application(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if app.UID != ctx.Doer.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
app.ClientSecret = ""
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ func GetGPGKey(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
key, err := asymkey_model.GetGPGKeyByID(ctx.ParamsInt64(":id"))
|
||||
key, err := asymkey_model.GetGPGKeyForUserByID(ctx.Doer.ID, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrGPGKeyNotExist(err) {
|
||||
ctx.NotFound()
|
||||
|
||||
@@ -62,6 +62,11 @@ func GetHook(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin && hook.OwnerID != ctx.Doer.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
|
||||
+50
-51
@@ -19,81 +19,80 @@ import (
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
)
|
||||
|
||||
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
|
||||
func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
funcInfo := routing.GetFuncInfo(storageHandler, prefix)
|
||||
return func(next http.Handler) http.Handler {
|
||||
if storageSetting.MinioConfig.ServeDirect {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
|
||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
rPath = util.PathJoinRelX(rPath)
|
||||
|
||||
u, err := objStore.URL(rPath, path.Base(rPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||
http.Error(w, "file not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
})
|
||||
}
|
||||
|
||||
if storageSetting.MinioConfig.ServeDirect {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
next.ServeHTTP(w, req)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
|
||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
rPath = util.PathJoinRelX(rPath)
|
||||
if rPath == "" || rPath == "." {
|
||||
http.Error(w, "file not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := objStore.Stat(rPath)
|
||||
u, err := objStore.URL(rPath, path.Base(rPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||
http.Error(w, "file not found", http.StatusNotFound)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fr, err := objStore.Open(rPath)
|
||||
if err != nil {
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
})
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
|
||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
rPath = util.PathJoinRelX(rPath)
|
||||
if rPath == "" || rPath == "." {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := objStore.Stat(rPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fr, err := objStore.Open(rPath)
|
||||
if err != nil {
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func requireSignIn(ctx *context.Context) {
|
||||
if !setting.Service.RequireSignInView {
|
||||
return
|
||||
}
|
||||
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
ctx.Error(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func gitHTTPRouters(m *web.Route) {
|
||||
m.Group("", func() {
|
||||
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
|
||||
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
|
||||
m.GetOptions("/info/refs", repo.GetInfoRefs)
|
||||
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
|
||||
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
|
||||
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
|
||||
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
|
||||
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
|
||||
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
|
||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
|
||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
|
||||
}, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
|
||||
}
|
||||
@@ -251,7 +251,6 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
||||
isSameRepo = true
|
||||
ci.HeadUser = ctx.Repo.Owner
|
||||
ci.HeadBranch = headInfos[0]
|
||||
|
||||
} else if len(headInfos) == 2 {
|
||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||
if len(headInfosSplit) == 1 {
|
||||
@@ -406,6 +405,9 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
|
||||
return nil
|
||||
}
|
||||
defer ci.HeadGitRepo.Close()
|
||||
} else {
|
||||
ctx.NotFound("ParseCompareInfo", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["HeadRepo"] = ci.HeadRepo
|
||||
|
||||
@@ -2968,6 +2968,11 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
@@ -3034,6 +3039,11 @@ func DeleteComment(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
@@ -3160,6 +3170,11 @@ func ChangeCommentReaction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
@@ -3303,6 +3318,21 @@ func GetCommentAttachments(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if !comment.Type.HasAttachmentSupport() {
|
||||
ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
|
||||
return
|
||||
|
||||
@@ -118,7 +118,7 @@ func GetContentHistoryDetail(ctx *context.Context) {
|
||||
}
|
||||
|
||||
historyID := ctx.FormInt64("history_id")
|
||||
history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID)
|
||||
history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, map[string]any{
|
||||
"message": "Can not find the content history",
|
||||
|
||||
@@ -467,7 +467,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound("", nil)
|
||||
|
||||
+26
-11
@@ -592,7 +592,31 @@ func DeleteTag(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
||||
if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil {
|
||||
redirect := func() {
|
||||
if isDelTag {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"redirect": ctx.Repo.RepoLink + "/tags",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"redirect": ctx.Repo.RepoLink + "/releases",
|
||||
})
|
||||
}
|
||||
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.NotFound("GetReleaseForRepoByID", err)
|
||||
} else {
|
||||
ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
|
||||
redirect()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil {
|
||||
if models.IsErrProtectedTagName(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||
} else {
|
||||
@@ -606,14 +630,5 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
||||
}
|
||||
}
|
||||
|
||||
if isDelTag {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"redirect": ctx.Repo.RepoLink + "/tags",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"redirect": ctx.Repo.RepoLink + "/releases",
|
||||
})
|
||||
redirect()
|
||||
}
|
||||
|
||||
@@ -821,6 +821,11 @@ func UsernameSubRoute(ctx *context.Context) {
|
||||
reloadParam := func(suffix string) (success bool) {
|
||||
ctx.SetParams("username", strings.TrimSuffix(username, suffix))
|
||||
context_service.UserAssignmentWeb()(ctx)
|
||||
// check view permissions
|
||||
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||
ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
|
||||
return false
|
||||
}
|
||||
return !ctx.Written()
|
||||
}
|
||||
switch {
|
||||
|
||||
@@ -422,7 +422,7 @@ func PackageSettingsPost(ctx *context.Context) {
|
||||
|
||||
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
|
||||
// redirect to the package if there are still versions available
|
||||
if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID}); has {
|
||||
if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
|
||||
redirectURL = ctx.Package.Descriptor.PackageWebLink()
|
||||
}
|
||||
|
||||
|
||||
+3
-14
@@ -174,6 +174,8 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||
return routes
|
||||
}
|
||||
|
||||
var ignSignInAndCsrf = auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
||||
|
||||
// registerRoutes register routes
|
||||
func registerRoutes(m *web.Route) {
|
||||
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
|
||||
@@ -181,7 +183,6 @@ func registerRoutes(m *web.Route) {
|
||||
// TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView)
|
||||
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
|
||||
ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
|
||||
ignSignInAndCsrf := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
|
||||
validation.AddBindingRules()
|
||||
|
||||
linkAccountEnabled := func(ctx *context.Context) {
|
||||
@@ -1377,19 +1378,7 @@ func registerRoutes(m *web.Route) {
|
||||
})
|
||||
}, ignSignInAndCsrf, lfsServerEnabled)
|
||||
|
||||
m.Group("", func() {
|
||||
m.PostOptions("/git-upload-pack", repo.ServiceUploadPack)
|
||||
m.PostOptions("/git-receive-pack", repo.ServiceReceivePack)
|
||||
m.GetOptions("/info/refs", repo.GetInfoRefs)
|
||||
m.GetOptions("/HEAD", repo.GetTextFile("HEAD"))
|
||||
m.GetOptions("/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
|
||||
m.GetOptions("/objects/info/http-alternates", repo.GetTextFile("objects/info/http-alternates"))
|
||||
m.GetOptions("/objects/info/packs", repo.GetInfoPacks)
|
||||
m.GetOptions("/objects/info/{file:[^/]*}", repo.GetTextFile(""))
|
||||
m.GetOptions("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", repo.GetLooseObject)
|
||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", repo.GetPackFile)
|
||||
m.GetOptions("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", repo.GetIdxFile)
|
||||
}, ignSignInAndCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
|
||||
gitHTTPRouters(m)
|
||||
})
|
||||
})
|
||||
// ***** END: Repository *****
|
||||
|
||||
@@ -37,12 +37,16 @@ func isContainerPath(req *http.Request) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
gitRawReleasePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/))`)
|
||||
lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
|
||||
gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`)
|
||||
lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
|
||||
)
|
||||
|
||||
func isGitRawReleaseOrLFSPath(req *http.Request) bool {
|
||||
if gitRawReleasePathRe.MatchString(req.URL.Path) {
|
||||
func isGitRawOrAttachPath(req *http.Request) bool {
|
||||
return gitRawOrAttachPathRe.MatchString(req.URL.Path)
|
||||
}
|
||||
|
||||
func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
|
||||
if isGitRawOrAttachPath(req) {
|
||||
return true
|
||||
}
|
||||
if setting.LFS.StartServer {
|
||||
|
||||
@@ -85,6 +85,10 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||
"/owner/repo/releases/download/tag/repo.tar.gz",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"/owner/repo/attachments/6d92a9ee-5d8b-4993-97c9-6181bdaa8955",
|
||||
true,
|
||||
},
|
||||
}
|
||||
lfsTests := []string{
|
||||
"/owner/repo/info/lfs/",
|
||||
@@ -104,11 +108,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil)
|
||||
setting.LFS.StartServer = false
|
||||
if got := isGitRawReleaseOrLFSPath(req); got != tt.want {
|
||||
if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
|
||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
setting.LFS.StartServer = true
|
||||
if got := isGitRawReleaseOrLFSPath(req); got != tt.want {
|
||||
if got := isGitRawOrAttachOrLFSPath(req); got != tt.want {
|
||||
t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
@@ -117,11 +121,11 @@ func Test_isGitRawOrLFSPath(t *testing.T) {
|
||||
t.Run(tt, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", tt, nil)
|
||||
setting.LFS.StartServer = false
|
||||
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawReleasePathRe.MatchString(tt))
|
||||
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt))
|
||||
}
|
||||
setting.LFS.StartServer = true
|
||||
if got := isGitRawReleaseOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer {
|
||||
t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer)
|
||||
}
|
||||
})
|
||||
|
||||
+23
-3
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,7 @@ func (b *Basic) Name() string {
|
||||
// Returns nil if header is empty or validation fails.
|
||||
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -132,11 +133,30 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
|
||||
store.GetData()["SkipLocalTwoFA"] = true
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Basic Authorization: Logged in user %-v", u)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func 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(req.Header.Get("X-Gitea-OTP")); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return util.NewInvalidArgumentErrorf("invalid provided OTP")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -216,31 +215,6 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
|
||||
})
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned && ctx.IsBasicAuth {
|
||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
||||
return // Skip 2FA
|
||||
}
|
||||
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
return // No 2FA enrollment for this user
|
||||
}
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
||||
ok, err := twofa.ValidateTOTP(otpHeader)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in user is allowed to call APIs.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.AdminRequired {
|
||||
|
||||
@@ -128,7 +128,7 @@ func (o *OAuth2) userIDFromToken(tokenSHA string, store DataStore) int64 {
|
||||
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
|
||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
|
||||
!gitRawReleasePathRe.MatchString(req.URL.Path) {
|
||||
!isGitRawOrAttachPath(req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
|
||||
}
|
||||
|
||||
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
|
||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
|
||||
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
|
||||
handleSignIn(w, req, sess, user)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
@@ -16,12 +13,7 @@ func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
|
||||
}
|
||||
|
||||
func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
||||
if attach.CustomDownloadURL != "" {
|
||||
return attach.CustomDownloadURL
|
||||
}
|
||||
|
||||
// /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}
|
||||
return setting.AppURL + "api/repos/" + repo.FullName() + "/releases/" + strconv.FormatInt(attach.ReleaseID, 10) + "/assets/" + strconv.FormatInt(attach.ID, 10)
|
||||
return attach.DownloadURL()
|
||||
}
|
||||
|
||||
// ToAttachment converts models.Attachment to api.Attachment for API usage
|
||||
|
||||
@@ -35,6 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
|
||||
Name: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
CreatedAt: pd.Version.CreatedUnix.AsTime(),
|
||||
HTMLURL: pd.FullWebLink(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
||||
oldTitle := issue.Title
|
||||
issue.Title = title
|
||||
|
||||
if oldTitle == title {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,6 +282,8 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
||||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
for _, asset := range rel.Attachments {
|
||||
assetID := asset.ID // Don't optimize this, for closure we need a local variable
|
||||
assetDownloadURL := asset.DownloadURL
|
||||
size := int(asset.Size)
|
||||
dlCount := int(asset.DownloadCount)
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
@@ -292,18 +294,18 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
|
||||
Created: asset.Created,
|
||||
DownloadURL: &asset.DownloadURL,
|
||||
DownloadFunc: func() (io.ReadCloser, error) {
|
||||
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, asset.ID)
|
||||
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hasBaseURL(asset.DownloadURL, g.baseURL) {
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
|
||||
if !hasBaseURL(assetDownloadURL, g.baseURL) {
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL)
|
||||
return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
|
||||
}
|
||||
|
||||
// FIXME: for a private download?
|
||||
req, err := http.NewRequest("GET", asset.DownloadURL, nil)
|
||||
req, err := http.NewRequest("GET", assetDownloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -309,6 +309,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
||||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
for k, asset := range rel.Assets.Links {
|
||||
assetID := asset.ID // Don't optimize this, for closure we need a local variable
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
ID: int64(asset.ID),
|
||||
Name: asset.Name,
|
||||
@@ -316,13 +317,13 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
|
||||
Size: &zero,
|
||||
DownloadCount: &zero,
|
||||
DownloadFunc: func() (io.ReadCloser, error) {
|
||||
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, asset.ID, gitlab.WithContext(g.ctx))
|
||||
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hasBaseURL(link.URL, g.baseURL) {
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL)
|
||||
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL)
|
||||
return io.NopCloser(strings.NewReader(link.URL)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err := pr.Issue.LoadPoster(ctx); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
|
||||
issueReference := "#"
|
||||
|
||||
@@ -291,17 +291,7 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod
|
||||
}
|
||||
|
||||
// DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
|
||||
func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, delTag bool) error {
|
||||
rel, err := repo_model.GetReleaseByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetReleaseByID: %w", err)
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRepositoryByID: %w", err)
|
||||
}
|
||||
|
||||
func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *repo_model.Release, doer *user_model.User, delTag bool) error {
|
||||
if delTag {
|
||||
protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID)
|
||||
if err != nil {
|
||||
@@ -334,19 +324,19 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del
|
||||
}, repository.NewPushCommits())
|
||||
notification.NotifyDeleteRef(ctx, doer, repo, refName)
|
||||
|
||||
if err := repo_model.DeleteReleaseByID(ctx, id); err != nil {
|
||||
if err := repo_model.DeleteReleaseByID(ctx, rel.ID); err != nil {
|
||||
return fmt.Errorf("DeleteReleaseByID: %w", err)
|
||||
}
|
||||
} else {
|
||||
rel.IsTag = true
|
||||
|
||||
if err = repo_model.UpdateRelease(ctx, rel); err != nil {
|
||||
if err := repo_model.UpdateRelease(ctx, rel); err != nil {
|
||||
return fmt.Errorf("Update: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
rel.Repo = repo
|
||||
if err = rel.LoadAttributes(ctx); err != nil {
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
return fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
|
||||
// Deliver deliver hook task
|
||||
func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
|
||||
w, err := webhook_model.GetWebhookByID(t.HookID)
|
||||
w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -239,7 +239,7 @@ var (
|
||||
hostMatchers []glob.Glob
|
||||
)
|
||||
|
||||
func webhookProxy() func(req *http.Request) (*url.URL, error) {
|
||||
func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) (*url.URL, error) {
|
||||
if setting.Webhook.ProxyURL == "" {
|
||||
return proxy.Proxy()
|
||||
}
|
||||
@@ -257,6 +257,9 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
|
||||
return func(req *http.Request) (*url.URL, error) {
|
||||
for _, v := range hostMatchers {
|
||||
if v.Match(req.URL.Host) {
|
||||
if !allowList.MatchHostName(req.URL.Host) {
|
||||
return nil, fmt.Errorf("webhook can only call allowed HTTP servers (check your %s setting), deny '%s'", allowList.SettingKeyHint, req.URL.Host)
|
||||
}
|
||||
return http.ProxyURL(setting.Webhook.ProxyURLFixed)(req)
|
||||
}
|
||||
}
|
||||
@@ -278,8 +281,8 @@ func Init() error {
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify},
|
||||
Proxy: webhookProxy(),
|
||||
DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil),
|
||||
Proxy: webhookProxy(allowedHostMatcher),
|
||||
DialContext: hostmatcher.NewDialContextWithProxy("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -14,35 +14,72 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/hostmatcher"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWebhookProxy(t *testing.T) {
|
||||
oldWebhook := setting.Webhook
|
||||
t.Cleanup(func() {
|
||||
setting.Webhook = oldWebhook
|
||||
})
|
||||
|
||||
setting.Webhook.ProxyURL = "http://localhost:8080"
|
||||
setting.Webhook.ProxyURLFixed, _ = url.Parse(setting.Webhook.ProxyURL)
|
||||
setting.Webhook.ProxyHosts = []string{"*.discordapp.com", "discordapp.com"}
|
||||
|
||||
kases := map[string]string{
|
||||
"https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx": "http://localhost:8080",
|
||||
"http://s.discordapp.com/assets/xxxxxx": "http://localhost:8080",
|
||||
"http://github.com/a/b": "",
|
||||
allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", "discordapp.com,s.discordapp.com")
|
||||
|
||||
tests := []struct {
|
||||
req string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
req: "https://discordapp.com/api/webhooks/xxxxxxxxx/xxxxxxxxxxxxxxxxxxx",
|
||||
want: "http://localhost:8080",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://s.discordapp.com/assets/xxxxxx",
|
||||
want: "http://localhost:8080",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://github.com/a/b",
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
req: "http://www.discordapp.com/assets/xxxxxx",
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.req, func(t *testing.T) {
|
||||
req, err := http.NewRequest("POST", tt.req, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
for reqURL, proxyURL := range kases {
|
||||
req, err := http.NewRequest("POST", reqURL, nil)
|
||||
assert.NoError(t, err)
|
||||
u, err := webhookProxy(allowedHostMatcher)(req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := webhookProxy()(req)
|
||||
assert.NoError(t, err)
|
||||
if proxyURL == "" {
|
||||
assert.Nil(t, u)
|
||||
} else {
|
||||
assert.EqualValues(t, proxyURL, u.String())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
got := ""
|
||||
if u != nil {
|
||||
got = u.String()
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,12 @@ func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
|
||||
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
|
||||
}
|
||||
|
||||
func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
||||
}
|
||||
|
||||
func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
|
||||
return &DingtalkPayload{
|
||||
MsgType: "actionCard",
|
||||
|
||||
@@ -256,6 +256,12 @@ func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
|
||||
}
|
||||
|
||||
// GetDiscordPayload converts a discord webhook into a DiscordPayload
|
||||
func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(DiscordPayload)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
type (
|
||||
// FeishuPayload represents
|
||||
FeishuPayload struct {
|
||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive
|
||||
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
@@ -158,6 +158,12 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
|
||||
func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(FeishuPayload), p, event)
|
||||
|
||||
@@ -230,6 +230,24 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
|
||||
return text, issueTitle, color
|
||||
}
|
||||
|
||||
func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
|
||||
refLink := linkFormatter(p.Package.HTMLURL, p.Package.Name+":"+p.Package.Version)
|
||||
|
||||
switch p.Action {
|
||||
case api.HookPackageCreated:
|
||||
text = fmt.Sprintf("Package created: %s", refLink)
|
||||
color = greenColor
|
||||
case api.HookPackageDeleted:
|
||||
text = fmt.Sprintf("Package deleted: %s", refLink)
|
||||
color = redColor
|
||||
}
|
||||
if withSender {
|
||||
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
|
||||
}
|
||||
|
||||
return text, color
|
||||
}
|
||||
|
||||
// ToHook convert models.Webhook to api.Hook
|
||||
// This function is not part of the convert package to prevent an import cycle
|
||||
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
|
||||
|
||||
@@ -210,6 +210,21 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
|
||||
return getMatrixPayload(text, nil, m.MsgType), nil
|
||||
}
|
||||
|
||||
func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
|
||||
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
var text string
|
||||
|
||||
switch p.Action {
|
||||
case api.HookPackageCreated:
|
||||
text = fmt.Sprintf("[%s] Package published by %s", repoLink, senderLink)
|
||||
case api.HookPackageDeleted:
|
||||
text = fmt.Sprintf("[%s] Package deleted by %s", repoLink, senderLink)
|
||||
}
|
||||
|
||||
return getMatrixPayload(text, nil, m.MsgType), nil
|
||||
}
|
||||
|
||||
// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
|
||||
func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(MatrixPayload)
|
||||
|
||||
@@ -296,6 +296,20 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||
), nil
|
||||
}
|
||||
|
||||
func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
||||
|
||||
return createMSTeamsPayload(
|
||||
p.Repository,
|
||||
p.Sender,
|
||||
title,
|
||||
"",
|
||||
p.Package.HTMLURL,
|
||||
color,
|
||||
&MSTeamsFact{"Package:", p.Package.Name},
|
||||
), nil
|
||||
}
|
||||
|
||||
// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
|
||||
func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(MSTeamsPayload), p, event)
|
||||
|
||||
@@ -104,6 +104,10 @@ func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetPackagistPayload converts a packagist webhook into a PackagistPayload
|
||||
func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
|
||||
s := new(PackagistPayload)
|
||||
|
||||
@@ -22,6 +22,7 @@ type PayloadConvertor interface {
|
||||
Repository(*api.RepositoryPayload) (api.Payloader, error)
|
||||
Release(*api.ReleasePayload) (api.Payloader, error)
|
||||
Wiki(*api.WikiPayload) (api.Payloader, error)
|
||||
Package(*api.PackagePayload) (api.Payloader, error)
|
||||
}
|
||||
|
||||
func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
|
||||
@@ -53,6 +54,8 @@ func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.
|
||||
return s.Release(p.(*api.ReleasePayload))
|
||||
case webhook_module.HookEventWiki:
|
||||
return s.Wiki(p.(*api.WikiPayload))
|
||||
case webhook_module.HookEventPackage:
|
||||
return s.Package(p.(*api.PackagePayload))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -171,6 +171,12 @@ func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
}
|
||||
|
||||
// Push implements PayloadConvertor Push method
|
||||
func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
|
||||
// n new commits
|
||||
|
||||
@@ -186,6 +186,12 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
|
||||
return createTelegramPayload(text), nil
|
||||
}
|
||||
|
||||
func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
|
||||
|
||||
return createTelegramPayload(text), nil
|
||||
}
|
||||
|
||||
// GetTelegramPayload converts a telegram webhook into a TelegramPayload
|
||||
func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(TelegramPayload), p, event)
|
||||
|
||||
@@ -179,6 +179,12 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
|
||||
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newWechatworkMarkdownPayload(text), nil
|
||||
}
|
||||
|
||||
// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
|
||||
func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
|
||||
return convertPayloader(new(WechatworkPayload), p, event)
|
||||
|
||||
@@ -325,8 +325,6 @@
|
||||
<!-- Environment Config -->
|
||||
<h4 class="ui dividing header">{{.locale.Tr "install.env_config_keys"}}</h4>
|
||||
<div class="inline field">
|
||||
<label></label>
|
||||
<button class="ui primary button">{{.locale.Tr "install.install_btn_confirm"}}</button>
|
||||
<div class="right-content">
|
||||
{{.locale.Tr "install.env_config_keys_prompt"}}
|
||||
</div>
|
||||
|
||||
Generated
+4
@@ -20029,6 +20029,10 @@
|
||||
"creator": {
|
||||
"$ref": "#/definitions/User"
|
||||
},
|
||||
"html_url": {
|
||||
"type": "string",
|
||||
"x-go-name": "HTMLURL"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
|
||||
@@ -35,6 +35,14 @@ func TestAPIGetCommentAttachment(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
|
||||
|
||||
@@ -140,12 +140,25 @@ func TestAPIEditComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
const newCommentBody = "This is the new comment body"
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"body": newCommentBody,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
@@ -164,12 +177,22 @@ func TestAPIEditComment(t *testing.T) {
|
||||
func TestAPIDeleteComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@@ -107,6 +108,26 @@ func TestAPICommentReactions(t *testing.T) {
|
||||
})
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestf(t, "GET", urlStr)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
// Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
|
||||
@@ -72,6 +72,17 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
|
||||
Content: rawKeyBody.Key,
|
||||
Mode: perm.AccessModeRead,
|
||||
})
|
||||
|
||||
// Using the ID of a key that does not belong to the repository must fail
|
||||
{
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d?token=%s", repoOwner.Name, repo.Name, newDeployKey.ID, token))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
session5 := loginUser(t, "user5")
|
||||
token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteRepository)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user5/repo4/keys/%d?token=%s", newDeployKey.ID, token5))
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReadWriteDeployKey(t *testing.T) {
|
||||
|
||||
@@ -35,6 +35,6 @@ func TestNodeinfo(t *testing.T) {
|
||||
assert.Equal(t, "gitea", nodeinfo.Software.Name)
|
||||
assert.Equal(t, 25, nodeinfo.Usage.Users.Total)
|
||||
assert.Equal(t, 18, nodeinfo.Usage.LocalPosts)
|
||||
assert.Equal(t, 2, nodeinfo.Usage.LocalComments)
|
||||
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -205,6 +205,56 @@ func TestIssueCommentClose(t *testing.T) {
|
||||
assert.Equal(t, "Description", val)
|
||||
}
|
||||
|
||||
func TestIssueCommentDelete(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||
comment1 := "Test comment 1"
|
||||
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||
assert.Equal(t, comment1, comment.Content)
|
||||
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{
|
||||
"_csrf": GetCSRF(t, session, issueURL),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetCSRF(t, session, issueURL),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID})
|
||||
}
|
||||
|
||||
func 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)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetCSRF(t, session, issueURL),
|
||||
"content": modifiedContent,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||
assert.Equal(t, modifiedContent, comment.Content)
|
||||
}
|
||||
|
||||
func TestIssueReaction(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestMirrorPull(t *testing.T) {
|
||||
|
||||
release, err := repo_model.GetRelease(repo.ID, "v0.2")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, release_service.DeleteReleaseByID(ctx, release.ID, user, true))
|
||||
assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true))
|
||||
|
||||
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
|
||||
assert.True(t, ok)
|
||||
|
||||
@@ -4,9 +4,11 @@ import {displayError} from './common.js';
|
||||
|
||||
const {mermaidMaxSourceCharacters} = window.config;
|
||||
|
||||
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
|
||||
const iframeCss = `:root {color-scheme: normal}
|
||||
body {margin: 0; padding: 0; overflow: hidden}
|
||||
#mermaid {display: block; margin: 0 auto}`;
|
||||
#mermaid {display: block; margin: 0 auto}
|
||||
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
||||
|
||||
export async function renderMermaid() {
|
||||
const els = document.querySelectorAll('.markup code.language-mermaid');
|
||||
|
||||
Reference in New Issue
Block a user